ReneWangritte

Travel, Photos, Gourmet, Living And Beyond ...

[PyCon 2017] 歡樂學 Python 位元組碼(byte code)

誰適合閱讀這篇文章:初階到中階 python 基礎的程式設計師,想深入淺出的了解 python 的位元組碼(bytecode)

PyCon 2016 有一場很有趣的演講,內容是關於介紹 Python 位元組碼(Python Bytecode),演講名稱就稱為 "Playing Bytecode with Python"。這場演講有趣的地方,在於它完全表達了 Python 社群的精神,也就是以 “Monty Python” 的方式來表達一個抽象複雜的概念。在有如勞萊與哈台兩人一搭一唱的方式,傳遞了如論語般藉由孔子和弟子們的對話傳遞了儒道的精神,或柏拉圖和色諾芬以與蘇格拉底對話錄的方式來闡述知識的進程。

Read more…

[Women in TED] Reshma Saujani:請教導女孩們勇敢而不是完美

圖片來源:延伸閱讀(Life Hack)

 
 
2015 年 一月,Pew Research 發表了一項研究結果[1]。在此研究中,研究者想要回答一個問題,大多數的美國人對於女性的領導能力不加懷疑,認為女性與男性有相等的能力擔任公司以及政府機關的重要職位。甚而,有些人認為女性有些特質,擔任領導職位,更勝男性。在這樣的前提下,女性在公司或政府機關擔負領導責任的職位比例,卻大幅少於男性。研究者們想知道,在大眾認知的層面上,是什麼樣的原因,造成這樣的差異。於是隨機選取將近兩千名的美國成人做了一項有關女性與領導能力的網路問卷調查。他們的研究中發現:領導相關的職務上失衡的性別比例,主要原因是來自公司和環境上對女性持有較高標準。
 

Read more…

[Women in TED] Hannah Fry: 愛情數學方程式

“遇見自己的靈魂伴侶似乎比中樂透還難.”看著娛樂新聞裡分分合合的螢幕情侶,一度如膠似漆的恩愛夫妻,轉眼間成為話不投機的公眾怨偶,王子和公主的童話婚姻在許多平常人的眼裡可以說是撲朔迷離,霧裡看花.然而,平常人的婚姻感情生活似乎也沒有簡單許多.看著逐年陡峭攀升的離婚率,許多人不禁時時刻刻的感嘆著:“我的真命天子 / 真命天女倒底在何方呢?” 然而與其無語問著蒼天,或求助於霞海城隍廟的月老婚姻仲介服務(可惜,月老並不包括售後及保固服務).不如讓數學家們以扎實的理論和可量化的分析,來為曠男怨女們指點一條雙人同行的幸福之路吧!

Read more…

[Level 3] Shingling, MinHashing and Common distance measure I

誰適合閱讀這篇文章:熟悉 Hash, Set, Tries (Prefix and Suffix Tree) 等資料結構和有志從事大量資料分析的電腦工程師

主要解決問題:給定一份文件,如何在網際網路的無盡文件大海中,找到相似的文件? 主要應用:偵測剽竊 (Plagiarism),搜尋引擎欲尋找鏡像網頁,網路購物或電影推薦系統中的協同過濾

綱要:

  1. 如何快速比較兩文件集合並提供量化結果:
    a. 如何定義相似度?
    b. 如何重新定義相似度比較問題為集合問題。
  2. 如何實現快速比較高相似度文件(第二篇)

  3. 應用相似搜尋於巨量資料: minHash and Locality-Sensitive Hashing (第三篇)

  4. 更多關於 Locality-Sensitive Hashing (末篇)

在閱讀這篇教學後,妳將會學到:

  1. 現行的文獻技術中,如何定義兩文件的相似度?
  2. 如何使用 Shingle 來轉換原問題為集合比較問題?
  3. 如何使用 Jaccard similarity 來比較文件的相似度?

在進入如何實作快速且有效比較大量文件之前,我們先來瞭解一下,在現行的文獻技術中,有哪些數值標準,可以用來量化兩份文件的相似程度。

給定兩個有著相同長度的字串,最簡單的方法,便是我們可以數有幾個不一樣的字母,來當作兩個字串相異的程度。
這就是 Hamming Distance 所採用的方法,簡單的舉例:

Harry Potter
Herry Potter
01000 000000 => 若相同字母則用 0,不同字母則用 1

由上可看得到字串一:"Harry Potter" 和字串二:"Herry Potter" 相異程度只有 1 (相似度則高達 10)。
同樣的比較另外兩個字串:

Harry Potter
Ferry Poster
11000 001000

嗯,用肉眼看都覺得差很多。不過電腦沒有肉眼可看,也沒有人卓越大腦具有的平行處理的能力。
所以請幫幫電腦瞭解,該怎麼用 Hamming distance 比較兩字串的相異程度吧!

from functools import reduce
from operator import add
def hamming_distance(str1, str2):
    assert(len(str1) == len(str2))
    return(reduce(add, [ch1 != ch2 for ch1, ch2 in zip(str1, str2)]))

str1, str2 = 'Harry Potter', 'Ferry Poster'
print('Hamming distance for \'{}\' and \'{}\' is {}'.format(
        str1, str2, hamming_distance(str1, str2)))
Hamming distance for 'Harry Potter' and 'Ferry Poster' is 3

Hamming distance 一開始是用在電信通訊時,檢查傳輸是否正確,通常數位傳輸都是以 0/1 來表示,一般的應用是用來比較兩個二元字串,如 01000... 和 10000...。但如我所舉的例子,其應用並不只限於二元字串,只要是長度相同的字串,皆可以用 Hamming distance 來表示相異程度。但這並不代表,長度相同的字串,都要用 Hamming distance 來表示。為什麼呢?

因為字串的編輯過程要比我們所看到的還要複雜。也就是,我們雖然看到 Harry Potter 和 Herry Potter 只相差一個字母,可是如果 Herry 其實是打字員將 Harry 先誤看成 Henry,又一時手誤打成了 Herry 呢?這時候,我們就需要第二種測量字串相異程度,Edit distance。

再繼續我們粗心打字員的例子,也就是 Harry 先經歷了刪除 ar 然後又新增成為 en,變成了 Henry。最後又將 n 取代成了 r,才成為我們現在所看到的 Herry。從 Harry 到 Herry 所需的編輯步驟,則包括了兩個刪除 (deletions),兩個新增 (insersions) 和一個取代 (replacement)。如果將編輯的歷史考慮在內,而不僅只是如 Hamming distance 就所觀察到的字串來計算相異程度,Herry 跟 Harry 的距離其實比我們所想像的還要遙遠哪!

而定義有效的編輯步驟,則因應用而異。一般最簡單的應用僅會考慮,刪除和新增,因為取代本身就包含了一個刪除和一個新增。但在生物資訊的應用上,通常會假設突變取代的可能。也就是若以 DNA 序列為例,與其將鹼基的突變,看成兩個步驟,其實當作一個步驟的可能性較高,所以又另外增加了取代的編輯步驟。此外,用 Edit distance,我們再也不需要需要長度相同這個限制了,因為刪除和新增的兩個編輯步驟,我們可以任意比較兩個長度不同的字串了。

現在,我們需要再回到 Hamming distance 上。和 Hamming distance 觀念上相近,但測量的是兩集合(set)的相似程度,一般則是使用 Jaccard Similarity。如果,今天我們比較的不是兩個字串,而是兩份文件呢?在講到比較文件前,讓我們回到學生時代,老師要檢查學生的出席時,是怎麼做的呢?

一般當然是按照學生名冊,逐一比對出席的同學,然後看有多少同學是有出席,有多少是缺席的。這個比較相同的過程,就涉及了求交集(intersection) 的觀念。

我們可以將老師的學生名冊和在教室裡的學生當作是兩個集合,這兩個集合都不包含重複的學號或學生,換句話說兩集合中的元素都是獨一無二的,但兩集合卻可以找到一對一,或一對無的對映。一對無的情況可以試想,有學生退選這門課,但學生名冊沒有立即更新,或沒有註冊這門課的學生來旁聽。

所以,如果我們感興趣的問題是,有多少學生出席今天的課?那麼我們就可以計算在出席學生佔註冊學生和旁聽學生的比例。在分母的部份,也就是計算出席學生和註冊學生的共同集合則是求聯集的觀念 (union)。我們可以根據常理推斷,總出席人數絕對不會多於兩集合的聯集,也就是比例會在 0 和 1 的封閉區間內,其值不會大於 1 或小於 0。因為使用交集和聯集的比例,所有不同大小的兩集合相似度,都可以映射到 0 和 1 的數值(可是,這樣會有個小問題,來猜猜看是什麼吧?註1),所以我們也可以放心的用這個比例來當作計量方法進行比較。

既然,我們已經從 Hamming distance 的字串比較,升級到比較集合的觀念。但在班級的比喻中,我們對於集合中的元素,有一個假設,辨識集合中的元素不能重複,事實上也是如此,因為沒有兩個人是相同的,這樣不擁有重複元素的集合,叫做 Set。但是,在比較字串中,這樣的集合顯然是不會發生的,於是我們就需要延伸 Jaccard similarity 的計算到能擁有重複元素的集合,這樣的資料結構稱為 Bag 或是 Multiset,而兩 Bags 之間的 Jaccard similarity 則又稱為 Jaccard Bag Similarity。在定義 Jaccard Bag Similarity 時,求交集可以用求每個元素出現在兩集合的最小頻率,而聯集的部份則有兩個做法,一是用每個元素在兩集合出現頻率的總和,但這樣的問題是, Jaccard Bag similarity 的最大值,就不再是 1 而是 1/2。為了能繼續維持 Jaccard similarity 的特性,用最大頻率來代表聯集,較常使用於不同元素數量的 Bag 比較。

拿書上的練習來作例子[1, Exercise 3.1.2]:假設有三個 bags 分別為:

A = {1, 1, 1, 2}
B = {1, 1, 2, 2, 3}

則若將 A, B 當作不可有重複元素的 Set 來看,集合會變成:
A = {1, 2}
B = {1, 2, 3}

則 Jaccard similarity 為 $\frac{A \cap B}{A \cup B} = \frac{\{1, 2\}}{\{1, 2, 3\}}= \frac{2}{3}$
但若是將 A, B 當作可有重複元素的 Bag/MultiSet 來看,則 Jaccard Bag Similairity 則為:
$\frac{A \cap B}{A \cup B} =\ \frac{min(A, B)}{max(A, B)} =\ \frac{2+1+0}{3+2+1}=\frac{1}{2}$

看起來就沒有這麼相似啦!這就是 Jaccard Similarity 的定義。而簡單的 python code 和兩個例子代表的 Venn Graph 則是如下:

from functools import reduce
from operator import or_
from operator import and_
from operator import add
from itertools import zip_longest
from collections import namedtuple

def jaccard_similarity(str1, str2, allow_repeat=False):
    if not allow_repeat:
        if type(str1) == set and type(str2) == set:
            uniqs = [str1, str2]
        uniqs = list(map(set, [str1, str2]))
        return(len(reduce(and_, uniqs))/len(reduce(or_, uniqs)))
    else:
        freqs = {k: [0, 0] 
                for k in reduce(or_, list(map(set, [str1, str2])))}
        for ch1, ch2 in zip_longest(str1, str2):
            if ch1 is not None:
                freqs[ch1][0] += 1
            if ch2 is not None:
                freqs[ch2][1] += 1
        return(reduce(add, map(min, freqs.values()))/ reduce(add, map(max, freqs.values())))
        
str1, str2 = '1112', '11223'
print('Jaccard Similarity for sets \'{}\' and \'{}\' is {:.2f}:'.format(
        str1, str2, jaccard_similarity(str1, str2)))
print('Jaccard Similarity for bags \'{}\' and \'{}\' is {:.2f}:'.format(
        str1, str2, jaccard_similarity(str1, str2, True)))
Jaccard Similarity for sets '1112' and '11223' is 0.67:
Jaccard Similarity for bags '1112' and '11223' is 0.50:
from IPython.display import Image
Image(filename='plot.png')

再進入 Shingle 的觀念前,讓我們稍微整理一下之前所說的;

Hamming distance 是量測兩個相同長度的字串相異性,可以把兩個字串當成兩個字母的集合,而字母則是集合中的元素。 Jaccard similarity 是量測兩個集合的相似性,比較兩集合中相同的元素,並將相似程度量化成在 0 到 1 的比例。 Edit distance 是在將編輯歷史考慮在內的情況下量測兩任意字串相異性,簡單的說,就是定義 A 字串需要多少編輯步驟才能變成 B 字串。

先有了集合的概念後,和如何比較集合間的相似,接下來就是如何定義集合內元素。定義集合內的元素之所以重要,是因為元素的定義會影響比較的方法,規模,最終會影響集合比對的結果。在文字探勘的領域中,Shingling 就是一個常用的技術 [註 2]。Shingle 原本意思是鵝卵石,通常也可以當作建築的材料,在這裡可以看作是建立集合的基石。Shingle 可以是一個字彙,也可以是一個字母,可以是 K 個重疊且相連(overlapping and consecutive)的字母,或 K 個重疊且相連的字彙,端看應用為何?在網頁搜尋的應用中,多半是在字元的層級上來做分析,所以在錯別字或是同義字替換的容忍度上,沒有這麼高,也就無法作為語意理解的學習模型,但在低層級的比較上,如文件分析(Textual Analysis)卻已足夠。通常以單一 Shingle 作為集合中的元素,會造成集合間的低變異,因為較少的可用元素組合。試想,以英文字母作為 Shingle 來比較文件,若不考慮標點符號,總共只有 26 個選擇,以及若以 K 個字母作為 K-Shingle,則有 $26^K$ 個選擇,顯然地,後者可以組合的集合數目要大的多了!

我們現在稍微改變一下書中現成的例子[Example 3.4],來說明定義 Shingle 對於比較集合相似度的影響:
句子一: "the plane was ready for touch down"
句子二: "the quarterback was ready for scoring a touchdown"
皆不考慮大小寫,計算 Jaccard Similarity 的結果如下:

Definition of Shingles Jaccard Similarity
以單一字母為 1-Shingle,不考慮空白 0.67
以 9 個字母為 9-Shingles,考慮空白 0.12
以 9 個字母為 9-Shingles,不考慮空白 0.08
以 3 個字彙為 3-Shingle words,不考慮 stop words 0.10
以 3 個字彙為 3-Shingle words,考慮 stop words [註 3] 0.20
def shingle(k, doc, remove_whitespaces = False):
    """ calculate shingle given k by characters

    >>> sorted(shingle(2, ["abcdabd"]))
    ['ab', 'bc', 'bd', 'cd', 'da']
    >>> shingle(9, ["The plane was ready for touch down"]) \
    ... & shingle(9, ["The quarterback scored a touchdown"])
    set()
    >>> shingle(9, ["The plane was ready for touch down"], True)\
    ... & shingle(9, ["The quarterback scored a touchdown"], True)
    {'touchdown'}
    """

    shingles = set()
    for line in doc:
        if remove_whitespaces:
            line = line.replace(" ","")
        for i in range(0, len(line) - k + 1):
            shingles.add(line[i:i+k])

    return(shingles)

def word_shingle(k, doc, use_stopword=True):
    """ calculate shingle given k by words

    >>> set(["A spokesperson for", "for the Sudzo", "the Sudzo Corporation", 
    ... "that studies have", "have shown it", "it is good", "is good for", 
    ... "for people to", "to buy Sudzo"]) == word_shingle(3,
    ... ["A spokesperson for the Sudzo Corporation revealed today "
    ... "that studies have shown it is good for people to buy Sudzo products."])
    True
    >>> set(["A spokesperson for", "spokesperson for the", "for the Sudzo", 
    ... "the Sudzo Corporation"]) == word_shingle(3,
    ... ["A spokesperson for the Sudzo Corporation"], False)
    True
    """

    from nltk.corpus import stopwords
    from nltk.tokenize import word_tokenize

    shingles = set()
    for line in doc:
        tokens = word_tokenize(line)
        for i in range(0, len(tokens) - k + 1):
            if use_stopword == True and tokens[i].lower() \
              in stopwords.words('english'):
                shingles.add(' '.join(tokens[i:i+k]))
            if not use_stopword:
                shingles.add(' '.join(tokens[i:i+k]))
            i = i + 1
    return(shingles)

def print_out(k, str1, str2, call_back, **kwargs):
    print("========")
    results = [call_back(k, [s], **kwargs) for s in [str1, str2]]
    print("the {}-shingle set from str1 is \n({})".format(k, ','.join(results[0])))
    print("the {}-shingle set from str2 is \n({})".format(k, ','.join(results[1])))
    print("the Jaccard similarity between two is {:.2f}".format(
        jaccard_similarity(*results)))

str1, str2 = "the plane was ready for touch down", \
  "the quarterback was ready for scoring a touchdown"
print_out(1, str1, str2, shingle, remove_whitespaces=True)
# comment 9-shingle output for clean output
# print_out(9, str1, str2, shingle)
# print_out(9, str1, str2, shingle, remove_whitespaces=True)
print_out(3, str1, str2, word_shingle, use_stopword=False)
print_out(3, str1, str2, word_shingle)
========
the 1-shingle set from str1 is 
(u,e,r,y,l,h,p,n,s,d,o,f,w,a,c,t)
the 1-shingle set from str2 is 
(g,i,u,e,r,q,y,h,b,s,d,o,n,f,w,a,c,k,t)
the Jaccard similarity between two is 0.67
========
the 3-shingle set from str1 is 
(the plane was,was ready for,for touch down,plane was ready,ready for touch)
the 3-shingle set from str2 is 
(ready for scoring,scoring a touchdown,quarterback was ready,was ready for,the quarterback was,for scoring a)
the Jaccard similarity between two is 0.10
========
the 3-shingle set from str1 is 
(the plane was,was ready for,for touch down)
the 3-shingle set from str2 is 
(was ready for,the quarterback was,for scoring a)
the Jaccard similarity between two is 0.20

不過,定義了 Shingle 只是向較為完備的語言分析跨進了一小步,面對廣袤的文件大海,兩兩比對文件的相似度,仍會造成如天文數字般可怕的計算數目,所以在實務巨量資料的應用上,我們仍然需要非常快速的方法來實現,這些技巧將會在下面幾篇提到。

最後,希望本文能引起許多女孩對於演算法的興趣,同時對平日相當散漫的我,能有效釐清一些困惑。若本文有任何不盡完善的地方,敬請批評指教囉!

註 1: 兩集合可以擁有相當高的 Jaccard Similarity,但不一定具有統計上的相似意義(statistical significance)。試想,若因為取樣誤差,兩集合都只包括一個元素,那麼 Jaccard Similiaryt 不是 1 就是 0,這樣計算出來的結果,誤導性就相當高了!
註 2: Shingle 和文字探勘領域中經常使用的 n-gram,在 wiki 上的區別有點讓人感到困惑。Shingle 此詞最早是出現在 Border et al. 1997 年的文章,並沒有特別指出 Shingle 必須是字詞為單位,相反的只是定義 Shinge 為相連的子字串。(a contiguous subsequence)。事實上,我懷疑只是兩個可互相交換而不改變意義的專有名詞,只是 n-gram 的應用領域較廣,較為人知罷了!
註 3: stop words 指的是語言模型中經常使用,但在文件比較中不具有識別性的效果。如定冠詞 the 和不定冠詞 a。

Reference: [1] Ch 3 Finding Similar Measure in Mining Massive Datasets by Jure Leskovec, Anand Rajaraman, Jeffrey D. Ullman Coursera Mining Massive Datasets course (Ch 3-1, 3-2 and some sections from 3-5)

[Women in TED] Fei Fei Li - 我們如何教導電腦瞭解影像

Fei Fei Li 的 TED Talk (圖片來源: TED Talk )

次大戰期間,於布萊切利莊園祕密從事破解德軍密碼的艾倫圖靈(Alan Turing),在戰後醉心於發展有限狀態自動機械的抽象數學模型。同時能將數學符號與繁瑣的計算驟在腦中呈現的他,反覆思索自動機械的潛能,多少白晝和深夜的冥想思索,他將思考的結晶發表成計算機制和智慧(Computing Machinery and Intelligence)一文,在文中更提出了著名的圖靈測試,作為判斷計算機械是否能擁有智慧的準繩。這個問題,對於當時,需要以將近一個空房來放置一臺電腦,經常要動員好幾十位程序員進行穿孔和裝換紙帶的工作,方能編寫數行程式碼。機械能夠思考,似乎難以想像。但是對於圖靈,以及他所執筆一系列關於自動有限狀態機械的理論論文,文中獨特的視野和無邊的想像,再以抽象數學表達和理性邏輯論述,對未來的智慧機械勾勒出了遠景和藍圖。甚而,為了避免全面定義智慧這樣複雜而又模糊的概念,他提議以一種”模仿遊戲”(imitating game)來回答”機械是否擁有智慧”這個大哉問。

Read more…

[Chickenosaurus] 格蘭博士的侏羅"雞"公園

圖片左方身穿橘色襯衫的就是電影侏羅紀公園中的顧問也是故事中格蘭博士的原型。和 Smithsonian’s 自然歷史博物館的館長開懷大笑,難道是活生生的恐龍即將誕生了嗎?(圖片來源:華盛頓郵報)

電影侏羅紀公園剛推出的時候,我和許多的恐龍迷一般對於電影中利用琥珀內的古蚊子所保存恐龍的 DNA (去氧核醣核酸),並進而讓恐龍重生一樣的興奮。試著幻想這樣的場景,有著長脖子的雷龍,優雅的漫步在原始的草原上,三角龍為了一得美女的芳心,而相互卯足全力較勁,翼手龍在空中盤旋,警告著粗魯的不速之客暴龍,正準備闖入大家平靜的生活。這些在現在看來似乎是神話般的場景,似乎得以藉由在琥珀中封存的基因物質,而有了重現的機會。然而,我和許多恐龍迷一樣,當得知就算在現實生活中能找到剛對恐龍飽餐一頓後,卻不幸跌入琥珀中,成為將恐龍遺傳物質完整不缺的保存,蚊子歷史傳說中的木乃伊,也因為數量過於微小,無法讓恐龍的基因充分表現,而讓恐龍生龍活虎,而感到失落。

Read more…

[Islamophobia] 給查理,逝去的還有仍然活著的

我相信你,你相信我嗎?請給我一個擁抱(圖片來源

個人在家時,或許是喜歡電視裡熱絡的氣氛,讓我跟世界又有了連結,莫名養成了電視新聞配午飯的習慣。今日一如往常,打開電視,先是總吵吵鬧鬧,靠評論政治人物維生的名嘴們,仍是喋喋不休,臆測和總統大選有緋聞的一干政治人物的真正居心。我對這種信口拈來毫無根據的說詞,不感興趣,便轉到新聞台,赫然發現伊斯蘭國又公佈最新一批斬首囚犯的錄影帶。

Read more…

Rene Wang on #News,

[Science] 後宮甄環傳和海獅之間的關係

Zalophus californianus at Moss Landing 3.jpg
成群的海獅圖片出處:Zalophus californianus at Moss Landing 3” by Brocken Inaglory - Own work. Licensed under CC BY-SA 3.0 via Wikimedia Commons

獅其實是這樣進入後宮甄環傳裡的。一天晚上,我和母親大人在電視前面觀看公視的生態節目。生態節目裡正好介紹年輕的公海獅,覬覦岸上成群的母海獅,伺機想找機會交配。無奈這群龐大的母海獅們,數量可能超過數十隻,都屬於一隻(沒錯!是一隻!)成年的公海獅。為了不想被成年公海獅海扁一頓,沒實力的年輕海獅,只好在海灘外的淺海域徘徊,希望逮到成年公海獅失神的時刻,偷偷和其中一隻母海獅交配成功。

我看著生態節目的介紹,突然幽幽的跟母親大人交換心得:“怎麼覺得成年公海獅有點像甄環傳裡清朝的皇帝?”坐擁佳麗三千人的中國皇帝,多像海獅社會裡的成年海獅,為了保證自己的優勢基因能夠傳遞到下一代去,而佔據了成群的母海獅。

Read more…

Rene Wang on

[Taiwan] 不一樣的蘭陽風情 - 烏石港和倒下的博物館

YiLan-07014.jpg
烏石港的漁港風情

充滿盆地和小山丘的臺灣北部,位於宜蘭的蘭陽平原,就像乳娘一般,以它豐沛的雨水和富碩的農產資源,蘊育著臺灣北部的城市們。不像國家首要行政區,充滿著舊蔣家政權遺留下來許多信義,博愛等官腔的名字。宜蘭的地名,保留了移民墾荒時期的特色,如頭城,蘇澳。

Read more…

[Taiwan] 與萍蓬草萍水相逢

Yellow-Water-Lily-08289.jpg
這是什麼花呢?

日來,在龍潭腳踏車步道與客家文化館旁,都可以看到像荷花池的小池塘裡,開地不是大而華貴的蓮花,而是這樣小而嬌嫩的黃花。本來經過時,都小黃荷花,小黃荷花這樣親切的叫著,直到有一天爸爸看到小黃荷花的照片,興奮的告訴我們,這些小黃荷花正是臺灣特有的萍蓬草,因為瀕臨絕種,所以很難在其他地方看到。

Read more…