黑人无码精品一区二区三区,国产成人免费8x人网站视频 http://www.builtinbookshelves.com 寶寶取名 公司起名 專家起名 周易起名 姓氏起名 Sat, 19 Nov 2022 02:29:49 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.2 http://www.builtinbookshelves.com/wp-content/uploads/2023/04/2023042403580774.png 算法 – 寶寶取名網(wǎng) http://www.builtinbookshelves.com 32 32 店名測試打分免費測試?店名測試打分免費測試吉兇! http://www.builtinbookshelves.com/archives/25944 Sat, 19 Nov 2022 02:29:40 +0000 http://www.builtinbookshelves.com/?p=25944 朋友們,我來了~

今天這一章還是講測試,基于屬性的測試(Property-Based Testing)。其實我感覺叫隨機驗證測試更通俗易懂一點。

我們日常自己測試的時候,會有一個問題,就是我們既是裁判,也是球員。因為我們的程序就是順著我們的思路寫的,測試也是按照同樣的思路來測試的。所以,我們可能通常很難測出我們自己寫出來的bug。

解決這個問題的一個辦法,就是把測試交給其他人。這也是測試這個崗位存在的意義之一。但是,如果我們把測試交給了其他人,上一章提到的,對測試的思考可以幫助更好地設計程序,這個好處就不存在了。

基于屬性測試

解決這個問題的辦法就是,把其他人,換成我們的計算機,讓計算機來幫我們自動測試。

之前關于契約或者合約(Contract)那一章中,提到了要給程序制定一個合同,規(guī)定了輸入的規(guī)則、輸出的規(guī)則、以及不變量(比如給一個數(shù)組排序,進去和出來的數(shù)組長度應該是不變的)。

那么合約和不變量在這里統(tǒng)稱為屬性(property),很抽象是不是?個人覺得它的定義并不重要,舉個例子就懂了。

比如我們要驗證一個list的排序,我們可以驗證這兩件事:1.輸入和輸出的list長度是否相等;2.是不是list里的每一個都比它前面一個大。

python寫出來就是這樣:

from hypothesis import given
import hypothesis.strategies as some

@given(some.lists(some.Integers()))
def test_list_size_is_invariant_across_sorting(a_list):
original_length = len(a_list)
a_list.sort()
assert len(a_list) == original_length

@given(some.lists(some.text()))
def test_sorted_result_is_ordered(a_list):
a_list.sort()
for i in range(len(a_list) – 1):
assert a_list[i] <= a_list[i + 1]

更重要的是,它利用了hypothesis這個模塊,@given(some.lists(some.integers()))會讓它在運行的時候,利用隨機的數(shù)值把同樣的方法運行100次。它會把出現(xiàn)錯誤的情況記錄下來。

對數(shù)器

之前在學習算法的時候,也接觸到了一個叫做對數(shù)器的概念,其實和這里的基于屬性測試,基本上是一件事情。

就是為了驗證我們的算法A寫對了,我們先寫一個絕對正確的算法B,不管效率,只管正確。然后用同樣的數(shù)據(jù)同時跑算法A和算法B。當然也是隨機跑很多次啦。如果兩個出現(xiàn)了不同的結果,那就說明算法A寫錯了。

思路基本相同,只不過,相對來說,可能這個基于屬性測試更嚴謹一點,比如,同樣是排序算法,我用我寫的冒泡排序算法來驗證我的選擇排序算法,萬一,我的冒泡排序也寫錯了呢?但是,如果我直接從根本上解析出排序(正序)就是后一個比前一個大,顯然是更不容易出問題的,其實也能更進一步地鍛煉我們尋找根本問題的能力。

當然啦,嚴格意義上來說,比較后一個比前一個大,這個本身也是一種算法。

實話說,要思考清楚有哪些屬性是要測試的,這件事本身就充滿了難度。如果你平時沒有這個習慣,突然讓你想,你會大腦一片空白的,就像是剛學編程那會,遇到了一個需求完全不知道從哪里下手的那種感覺。坦白的說,我現(xiàn)在就是這種狀態(tài),想必這也是需要刻意練習的。

這種隨機大量測試的方式,可以幫助我們測出一些邊界值,測出一些我們想不到的情況。往往最容易出問題的地方也是在邊界值的地方。就跟開車似的,車開起來了,通常都沒什么問題,但是起步可能會熄火,停車可能會倒不進去。

Java的相關框架

關于這個基于屬性測試的框架,我隨手搜了一搜,Python有的,沒理由咱們Java沒有,對不對?

一、找到兩個github上開源:

1.https://github.com/HypothesisWorks/hypothesis-java

2.https://github.com/quicktheories/QuickTheories

二、找到一個都已經(jīng)有自己網(wǎng)站的(雖然也有github):

https://jqwik.net/

三、還有一個直接是JUnit家的JUnit-Quickcheck(感覺上這個更香一點)

https://github.com/pholser/junit-quickcheck

我還沒來得及仔細研究,朋友們可以先自行研究起來~

另一個實例

書中其實還提到了另外一個更加實際一點的例子,但我個人覺得那算不上bug,它和實際的需求有關系。這邊簡單提一下吧,或許你也有不同的見解。

大概就是有一個倉庫類,它可以存放各種貨品,不同貨品有各自的數(shù)量,大概是這樣一個結構吧:List<Map<String,Integer>>。然后我們可以查詢某個貨品是否有庫存、查詢某個貨品還剩多少庫存、可以從中取出某個數(shù)量的貨品。

代碼如下:

class Warehouse:
def __init__(self, stock):
self.stock = stock

def in_stock(self, item_name):
return (item_name in self.stock) and (self.stock[item_name] > 0)

def take_from_stock(self, item_name, quantity):
if quantity <= self.stock[item_name]:
self.stock[item_name] -= quantity
else:
raise Exception("Oversold {}".format(item_name))

def stock_count(self, item_name):
return self.stock[item_name]

倉庫的初始化是這樣的:

wh = Warehouse({"shoes": 10, "hats": 2, "umbrellas": 0})

然后,同樣用了hypothesis來做批量測試,然后在調用take_from_stock( item_name='hats', quantity=3)這樣一組數(shù)據(jù)的時候報錯了。

作者說,在in_stock我們不應該只判斷庫存是不是大于0,而是要判斷它有沒有包含我們要拿取的貨品數(shù)。代碼應該改成這樣:

def in_stock(self, item_name, quantity):
return (item_name in self.stock) and (self.stock[item_name] >= quantity)

反正我是覺得這不算個bug吧,畢竟在真正獲取貨品的時候,就報錯了呀。要看我們對于in_stock這個方法本身的要怎么定義咯,是只需要知道它還有沒有庫存,還是需要知道它有沒有我需要的庫存。

雖然實際需求中,后者可能性更大,但是在take_from_stock方法里報錯,又有什么問題呢?(又或者,作者只是想舉個例子,是我太較真了)

尾聲

基于屬性的測試是對于單元測試的補充,對于單元測試的思考,可以讓我們思考代碼實現(xiàn)的其他方式。基于屬性的測試,可以讓我們更加清晰,我們的方法能干什么不能干什么,同時,也消除一些意外的情況。

如果,你還沒有把這兩種測試用起來,現(xiàn)在就趕緊用起來吧~

]]>
fuller是什么意思英語?fuller是什么意思! http://www.builtinbookshelves.com/archives/23444 Thu, 20 Oct 2022 17:07:29 +0000 http://www.builtinbookshelves.com/?p=23444 嘉賓介紹

克里斯托弗·貝澤梅克 Christoph Bezemek

奧地利格拉茨大學教授、法學院院長

Professorand Dean of theFaculty of Law at theKarl-Franzens-University Graz

克里斯托弗·貝澤梅克獲得耶魯大學法學院LL.M和維也納大學哲學學位,擔任歐洲、非洲和北美多所大學的客座教授。他的研究重點包括憲法、法律和政治理論,并就這些主題發(fā)表了大量著作。除此之外,他還是《維也納國際憲法法雜志》的聯(lián)合主編,以及哈特的“維也納法律哲學講座”系列的聯(lián)合主編。

Prof.Bezemek completed a postgraduate degree from Yale Law School as a Master of Laws(LL.M.). He also holds a degree in philosophy from the University of Vienna.He has held numerous visiting teaching appointments at Universities in Europe, Africa, and North America. His research focuses on Constitutional Law, Legal and Political Theory and he has published extensively on those topics. He is, among other things, the Co-Editor-in-Chief of the Vienna Journal on International Constitutional Law (ICL-Journal, published with De Gruyter) and a Co-Editor of Hart’s “Vienna Lectures on Legal Philosophy”-Series.

算法與法的概念

§ 1

陌生的領域

我們處于一個陌生的領域,至少學術上如此。我們這門學科是這樣的——在這一點上甚至可能獨此一家——在主題問題上并沒有達成一致?!笆裁词?a href="http://www.builtinbookshelves.com/archives/tag/%e6%b3%95%e5%be%8b" title="【查看含有[法律]標簽的文章】" target="_blank">法律?”即使不稱之為法學的最大問題,也是重大問題之一。這個問題意義重大,卻也不值一提。對大多數(shù)律師來說,“什么是法律”這個問題與他們日常工作的關系,就如同“什么是醫(yī)學”與醫(yī)生的關系一樣:那就是毫無關系。甚至,醫(yī)生定義醫(yī)學或許還要更容易一些。

所以,一般情況下即使我們不知道自己在處理什么也沒關系。但說“不知道”可能太苛刻了。從學科的角度來看,與其說我們“不知道”法律是什么,不如說我們不能就這一問題達成一致,因為我們對權利的基本假設和態(tài)度在主題表現(xiàn)方面的分歧太大了。這一點也值得注意,但通常也不值一提。畢竟,對于這一主題,擁有根本不同的基本假設和主張的律師也可以談論理論問題,而不會陷入巴比倫式的混亂。如果法律學者們不理解彼此,那并不是因為他們互相無法理解。

相應地,由于這樣一個事實:盡管存在各種的差異,但我們學科中的各個思想傳統(tǒng)都有一個共同的出發(fā)點。只是,就目前而言,在給定的語境中,真正重要的是,與“法律是什么”相比,人們很少談論“法是什么”。當一個人這樣做的時候,幾乎帶有一種懺悔的性質,因為它是法學的重大問題之一。

§ 2

巴比倫時期

在許多情況下,法和算法之間的這種相互關系如何表現(xiàn),取決于詢問對象。如果你問數(shù)學家,那么一切以結果為導向的、機械的、循序漸進的可以被理解為算法的指令——縱使再簡單——也在結構上比法律規(guī)范中包含的事實和法律后果的相互作用更為高級。或者換一種說法——一種或許更為簡單的說法:法律規(guī)范只是算法的一種應用或子案例,或者至少有些學者認為有些法律規(guī)范就是如此。例如,在讓-盧克·沙拜爾(Jean-Luc Chabert)編寫的《算法的歷史》(History of Algorithms)中,我們能讀到巴比倫人——回到巴比倫時——已經(jīng)使用算法來解決法律問題。

這種觀點并非沒有誘惑,來看看《漢謨拉比法典》第110節(jié)中的規(guī)范,根據(jù)該規(guī)范,不居住在修道院的圣役,如果經(jīng)營或出入啤酒店喝酒,要被處以火刑。

對于有些人來說,這種觀點也并非沒有誘惑。基于諸如此類的例子,人們想要理解在由大前提、小前提和結論組成的三段論中作為一種“編程語言”如何適用法律。

誠然,在這些觀點的背后隱藏著一種不成熟的形式主義,這種形式主義為現(xiàn)代實證主義思想家如漢斯·凱爾森(Hans Kelsen)所反對。只需將其與20世紀法律解釋中討論最多的問題作一個簡單的比較,就可以發(fā)現(xiàn)如何處理圣役,比如當她在啤酒店里喝水,當她是被迫喝啤酒,當她不是被迫但不是在啤酒店里而是在啤酒店前喝啤酒,或者當她自愿在啤酒店里喝啤酒但卻是在剛從修道院逃出后才喝的,所能提出的問題不比“公園內禁止車輛”這條規(guī)定上所能提出的問題更簡單。

這并不是否定三段論在法律論證中的整體作用。但這個例子意在強調,法律論證不僅僅像算盤算術一樣,“法律的開放結構”也不僅僅意味著沉溺于演繹推理。在這方面,“法律的生命(一直)不只是邏輯;不只是結果導向的機械論的步進指令的實施”。在古代巴比倫,就像在今天的奧地利一樣,正如霍姆斯大法官指出的,“在決定統(tǒng)治人類的規(guī)則時,時下所感受到的必要性、流行的道德觀念和政治觀點、公共政策的已知或無意識的直覺,甚至是規(guī)則制定者與他們的同胞共享的偏見,都比三段論更為重要?!?/p>

現(xiàn)在,我們能夠這樣說:法律不是一種算法。至少不是上面所描述的那種算法。

相反,這并不意味著算法不是法律——盡管這樣說略微有些無視同一性定理的意味。至少如果我們相信勞倫斯?萊斯格(Lawrence Lessig)的話,就是如此。萊斯格在1999年發(fā)表了關于網(wǎng)絡空間監(jiān)管特性的分析,正是在這篇文章中他推導出并引入了被多次引用的流行語“代碼即法律”。萊斯格認為,基于算法的代碼在虛擬現(xiàn)實中發(fā)揮了與法律監(jiān)管相同的功能?!霸诂F(xiàn)實世界中,我們意識到法律是如何監(jiān)管的——通過憲法、制定法和其他法律法典。在網(wǎng)絡空間,我們必須理解另一種“代碼”是如何監(jiān)管的——軟件和硬件(即網(wǎng)絡空間的“代碼”)構成了網(wǎng)絡空間,也同樣監(jiān)管著它。

這里的代碼是“法律的法典”。對此,我想說,這絕對是一種誤解。

在此,我再次引用萊斯格的一段話,讓這種誤解更清楚地突顯出來。在這段話中,萊斯格承認算法編碼和法律規(guī)范之間存在差異,但卻將其棄之不顧,以便進行比較。他解釋說:“霍姆斯大法官以把監(jiān)管機構的重點放在了‘壞人’身上聞名。他提出了一種以‘壞人’為核心的監(jiān)管理論。他的關鍵點并非每個人都是‘壞人’;相反,關鍵是我們如何才能最好地構建監(jiān)管體系。”他認為“我的觀點也是如此。我建議,如果我們考慮到監(jiān)管的‘程序人’理論,就能學到一些東西——它主要關注代碼監(jiān)管。”我承認,“壞人”和“程序人”是有趣的文字游戲。不過在我看來,它也就僅此而已。

霍姆斯在對美國“法律預測說”的建立產(chǎn)生了巨大影響的《法律的道路》中說的是什么?霍姆斯在萊斯格提到過的關鍵段落中這樣評論:“如果一個人只想了解法律而不關心其他,那么就必須把法律看成一個只關心這類知識能讓他預測到后果的壞人,而不是一個在良心的模糊約束下為他的行為尋找理由的好人,不管是在法律范圍內還是法律之外?!?/p>

那個壞人,是法學界被誤解最多的生物之一。在我看來,他一點都不壞,意思是:不是真的壞。即使我在這一領域的許多同事不這么認為。因為他絲毫沒有道德觀念,而不是不遵守道德。在法律以客觀的意義呈現(xiàn)給他的抽象事物的具象化中,他只是想知道他能走多遠,或者更確切地說,理論上能走多遠。壞人并不比教義學者更壞或更好,根據(jù)凱爾森(Kelson)的說法,教義學者在法律政策方面為行動和決策提供選擇。在這方面,壞人并不比她、我或我們所有人更好或更壞:像壞人一樣,我們能夠在這些選擇的基礎上做決定,來培養(yǎng)克里斯托弗·穆勒斯(Christoph M?llers)分別以積極和消極的術語恰如其分地描述的“規(guī)范的可能性”——遵循法律和違反法律之間的選擇,在這里——也是凱爾森的說法——法律第一次迎來它真正的考驗。

§ 3

程序人

但這與我們的問題以及在算法代碼調節(jié)效應下的“程序人”有多大關系?關系非常大。但要詳細說明,我們必須面對這樣一個大問題。為什么“代碼”不是“法律”?畢竟,我們的虛擬現(xiàn)實在最激烈的意義上表現(xiàn)為一種外部強制秩序,可以被理解為一種行為控制的“社會技術”;然而,具有決定性區(qū)別的是,這種外部強制秩序并不對應于那些受其約束的人的選擇自由。在萊斯格的理解中,調節(jié)效應——由算法代碼決定的環(huán)境——來自于現(xiàn)實的客觀秩序,即使它是虛擬現(xiàn)實,它限制了個體的行為,而不像上面描述的那樣隱藏任何“規(guī)范的可能性”。它的潛力是在事實中而不是在規(guī)范中展開的,其結果是,人不是主動地躺在床上,而是被動地躺在床上。如果這是一張普羅克拉斯提斯之床,它也一樣好。因此,“程序人”所受的控制效果與高速公路上的防撞護欄的控制效果或公園長凳上的防流浪漢護欄的控制效果沒有區(qū)別,它們可以被理解為一種針對汽車司機或社會上不受歡迎的人的社會技術。這里所使用的外部強制,當然不是凱爾森所設想的,當他定位動機時,他所考慮的動機是,約束守法和有效地避免規(guī)范所威脅的邪惡。是否服從它并不取決于個人。

因此,算法代碼的事實能力和創(chuàng)造世界的能力與法律秩序用來實現(xiàn)其控制主體的要求的世界內在規(guī)范能力之間存在張力。它的強制性——就像任何規(guī)范秩序的強制性一樣——預設了自愿性:即人不想犯罪。

§ 4

元宇宙中的富勒

現(xiàn)在有人可能會反對:這太過簡單化了。因為這充其量只是對創(chuàng)造的外部范圍的恰當描述。造物主如何對待他的創(chuàng)造物是另一回事。

讓我們簡要地看看馬克·扎克伯格對“元宇宙”的設想。在我看來,這是一個可怕的反烏托邦的虛擬環(huán)境,它完全復制了真實行動和互動的可能性,當然,應該遠不止于此。扎克伯格在介紹它時做了長達一個多小時的浮夸演示,講述了元宇宙中可能發(fā)生的事情。但這種可能性顯然不是前面提到的規(guī)范的可能性,而是現(xiàn)實的延伸,沿著它產(chǎn)生的事實發(fā)現(xiàn)它的限制。規(guī)范性要求是在由此產(chǎn)生的現(xiàn)實中明確、要求和執(zhí)行的,這一點絕不能被排除在外。為什么要這樣呢?直到今天,我還可以在Instagram上發(fā)布裸照。根據(jù)使用條款,我不應該這么做。如果我這么做了,我會受到制裁。但是,這并不構成在任何方面都具有結構意義的挑戰(zhàn),也不構成宣傳法律和代碼等同的理由。

偉大的美國法學家朗·富勒(Lon Fuller)關于法律秩序功能的觀點也適用于此,他把法律理解為“使人的行為服從規(guī)則治理的事業(yè)”。我們已經(jīng)討論了構成這種行為的所有先決條件和不確定因素。

照這一理解,同樣的情況也反映在一般層面上,即前面提到的自愿接受法律的強制?!?a href="http://www.builtinbookshelves.com/archives/tag/%e8%a7%84%e5%88%99" title="【查看含有[規(guī)則]標簽的文章】" target="_blank">規(guī)則”(廣義上)包含上述適用于政治共同體的規(guī)范性潛能,從形式上講,就是規(guī)范的可能性的整個視界。從實質上講,如霍姆斯所說,即前面提到的時下所感受到的必要性、流行的道德觀念和政治觀點、公共政策的已知或無意識的直覺,還有規(guī)則制定者與他們的同胞共同的偏見。

富勒認為,為了讓人類行為服從法治,它需要保證互惠性:立法者保證,正是這些規(guī)則衡量了那些受法律約束的人的行為,并與遵守或不遵守相應的后果聯(lián)系在一起(而不是其他)。法律秩序由此構成一個相互期待的可追究與問責的空間。能夠實現(xiàn)這些相互期望的先決條件是遵守某些結構原則,這些原則構成了富勒所說的“法律的內在道德”。這些原則包括法律規(guī)則中的必要公示和杜絕矛盾,避免溯及性立法,以及規(guī)則與執(zhí)行的對應。

然而,最重要的是,必須要有規(guī)則:一般來說,政治共同體遵循的抽象規(guī)范可以指導他們的行動。

在遵循這些結構性原則的同時,法律的內在道德傳遞著上述意義的政治外在道德。關于這種道德是否必須是嚴格意義上的“道德”這一問題則眾說紛紜,正如畢希納(Büchner)導演的電影《沃伊采克》(Woyzek)中上尉所說的那般。富勒認為這是必要的,但他的許多批評者并不這么認為。我傾向于同意他的批評者們,并假定這里所說的像適用于《歐洲人權公約》一樣適用于古巴比倫的法律,但我補充這一點僅僅是為了澄清。

§ 5

共享的政治道德

但是這些對于我的話題來說意味著什么?

首先,如果人們要遵循富勒的理解,將法律理解為“使人類行為服從規(guī)則治理的事業(yè)”(我承認這是一個寬泛的定義),那么就需要既可以遵守又可以無視的規(guī)則;簡而言之,一個基于前面提到過的個人自主性的規(guī)范性的可能性空間。安東尼·凱西(Anthony Casey)和安東尼·恩比列特(Anthony Nbilett)宣揚基于算法的微觀指令,用兩位作者的話來說就是:“精確定制的指令,具體規(guī)定在每一種特殊情況下允許做什么”,告知個人“在行動之前確切地如何遵守每一項相關法律”,不僅會導致“規(guī)則和標準的消亡”,就像他們兩人坦言承認的那樣。它們不符合上述意義上的法律,至少就它們否定個人的選擇是任何忠于法律的基本前提這一點來說是如此。

奧姆里·本-沙哈爾(Omri Ben-Shahar)和阿里爾·波拉特(Ariel Porat)在最近的一份出版物中以“針對不同人的不同規(guī)則”為題討論了在具體實施松散定義的目標時的個性化法律,無論如何,這與之前制定的標準明顯存在矛盾。碎片化的法律既是一個政治共同體的催化劑,也表明了一個政治共同體不再能夠在政治道德的外部領域中發(fā)現(xiàn)和了解自己。

然后,當一般立法開始侵蝕,個別立法的變形便不能停止。換句話說:如凱斯?桑斯坦(Cass Sunstein)和丹尼爾?卡尼曼(Daniel Kahneman)最近再次大力宣傳的,越來越多的法律決策轉向基于算法的系統(tǒng),就像其支持者所主張的那樣,可以輕松地(而且顯著地)減少“噪音”,即擾亂或影響我們決策的隨機因素。但代價是巨大的。我甚至不是在談論偏差問題或黑箱現(xiàn)象,這些算法決策要么延續(xù)程序員的偏見,要么使決策不透明。通過優(yōu)化算法可以減少偏差,黑盒可以打開,決策路徑也可以設法理解。這樣的決定缺乏以上文提到過的互惠性來區(qū)分立法的基本特征。他們缺乏對于理由的陳述,不是在不能理解它們的意義上,而是在要求參與從法律上實現(xiàn)的政治道德的共同領域的意義上。做出法律決定意味著參與這種政治道德,而在這種參與中,為建立在共同體的共同政治道德基礎上的判斷辯護,僅靠透明是無法做到的。

王 健 金惠珠

]]>
60行代碼實現(xiàn)經(jīng)典論文:0.7秒搞定泊松盤采樣,比Numpy快100倍 http://www.builtinbookshelves.com/archives/19343 Mon, 12 Sep 2022 19:54:39 +0000 http://www.builtinbookshelves.com/?p=19343

編輯整理自 太極圖形
量子位 | 公眾號 QbitAI

隨機均勻的點組成的圖案,在動植物身上已經(jīng)很常見了。

像楊梅、草莓、荔枝、紅毛丹這樣的水果,表面都有顆粒或者毛發(fā)狀的結構,它們隨機、均勻地散布在水果表面:

類似的圖案在動物身上也有,比如大家都愛涮的毛肚:

同樣地,在計算機模擬下,也有不少場景需要在空間中隨機、均勻地生成點。

像生成動物毛發(fā)時的毛孔位置、多人對戰(zhàn)游戲中的玩家出生位置、生成森林時的樹木位置等等。

這些場景的共同特點是,都需要讓任何兩點之間的距離大于等于一個下界(這個下界是預設的,改變它就可以控制生成點之間的間隔)。

但如果直接使用完全隨機生成的點,大概率會獲得一個很不均勻的分布結果,有些地方“扎堆”、有些地方稀疏:

如果用這樣的點來模擬毛發(fā)等位置生成,效果就很差。

所以,需要在生成點的過程中加入一個距離判斷,來剔除那些不合要求的點。

此前,用numpy生成這樣一個效果,往往需要70s左右,非常不劃算。

現(xiàn)在,太極圖形基于Taichi實現(xiàn)了一個超快算法,同樣的效果運行在單個CPU線程上,只需要0.7s就能生成這樣的圖案,快了100倍左右。

一起來看看他們是怎么做的。

采用Bridson算法實現(xiàn)

此前,有一種常見算法dart throwing (像一個人蒙上眼睛胡亂扔飛鏢的樣子)

每次在區(qū)域內隨機選擇一個點,并檢查該點與所有已經(jīng)得到的點之間是否存在“沖突”。

若該點與某個已得到的點的最小距離小于指定的下界,就拋棄這個點,否則這就是一個合格的點,把它加入已有點的集合。

重復這個操作直到獲得了足夠多的點,或者連續(xù)失敗了N次為止(N是某個設定的正整數(shù))。

但這種算法效率很低

因為隨著得到的點的個數(shù)增加,沖突的概率越來越大,獲得新的點所需的時間也越來越長,每次比較當前點和所有已有點之間的距離也會降低效率。

相比之下,Bridson算法則要更加高效。

這個算法的原理來自于Robert Bridson發(fā)表于2007年的論文”Fast Poisson Disk Sampling in Arbitrary Dimensions”[1](論文非常短,只有一頁A4紙),如果再去掉標題、引言的話,真正的算法內容只有一小段話。

開頭這個動圖,演示了Bridson圓盤采樣算法在一個400×400網(wǎng)格區(qū)域上的運行效果,算法嘗試獲得100K個均勻散布的點,實際生成了大約53.7K個:

這個動畫是使用Taichi生成的,運行在單個CPU線程上,除去編譯的時間計算,耗時僅在0.7s多一點,而同樣的代碼翻譯成NumPy要耗時70s左右。[2]

從上面的動畫效果可見,Bridson算法很像包菜的生長過程:我們從一個種子點開始,一層一層地向外添加新的點。

每一次我們添加的新的點,都位于最外層的點的周圍,并且盡可能地包住最外層。

為了避免每次都檢查和所有已有點之間的距離,Taichi采用了所謂網(wǎng)格的技巧:

將整個空間劃分為網(wǎng)格,對一個需要檢查的點,只要找到它所在的網(wǎng)格,然后檢查它和臨近網(wǎng)格中的點之間的最小距離即可。

只要這個距離大于指定的下界,更遠處的點就不必再檢查了。這個技巧在圖形學和物理仿真中是非常常用的。

這個采樣過程很難并行化,因為當一個線程“偷偷”加入一個新的點的時候,會改變其它所有線程對距離的判斷。所以Taichi僅使用單個CPU線程來執(zhí)行這個算法:

ti.init(arch=ti.cpu)

上面的代碼中通過指定arch=ti.cpu來讓程序運行在CPU上。

你可能會想,既然是單線程+CPU,那為什么不直接寫純Python呢?別著急,我們的計算部分會放在ti.kernel函數(shù)中,這種函數(shù)并不運行在Python虛擬機中,而是會被Taichi編譯執(zhí)行,所以會比純Python的實現(xiàn)快很多倍!

在我們介紹Bridson算法的具體實現(xiàn)之前,你不妨猜猜這個Taichi程序包含多少行代碼?

安裝和導入Taichi

首先推薦大家使用最新的Taichi發(fā)布版本,這樣可以使用更豐富的功能,在不同平臺上的支持也更穩(wěn)定。截止本文寫作時最新版本是1.0.3:

pip install taichi==1.0.3

然后,在代碼開頭寫上:

import taichi as ti
import taichi.math as tm

這樣會導入Taichi以及Taichi的math模塊。math模塊除了包含常用的數(shù)學函數(shù)之外,還提供了非常方便的向量運算。

準備工作

在泊松采樣算法中,采樣點之間的距離有一個下界r。

我們假設整個區(qū)域是由N×N個同樣大小的方格組成的網(wǎng)格區(qū)域,使得每個小方格的對角線長度正好是r,即網(wǎng)格的邊長是r/√2

于是任何小方格中至多包含一個點。如下圖所示:

這就是我們前面提到的網(wǎng)格化方法,即對于任何一個點p,設它所在的方格是D,則任何與p的距離小于等于r的點必然位于以D中心的、由5×5個方格組成的正方形區(qū)域中。

在檢查距離時,我們只要針對這個子區(qū)域進行計算即可。

我們用一個一維數(shù)組samples和一個N×N的二維數(shù)組grid來記錄已經(jīng)得到的采樣點:

  1. samples保存當前所有已經(jīng)采樣點的坐標,它的每個元素是一個二維坐標(x,y)。
  2. grid[i, j]是一個整數(shù),它存儲的是第(i, j)個方格中采樣點在數(shù)組samples中的下標。grid[i, j] = -1表示這個方格中沒有采樣點。

于是我們的初始設置可以這樣寫:

grid_n = 400
res = (grid_n, grid_n)
dx = 1 / res[0]
inv_dx = res[0]
radius = dx * ti.sqrt(2)
desired_samples = 100000
grid = ti.field(dtype=int, shape=res)
samples = ti.Vector.field(2, float, shape=desired_samples)

這里網(wǎng)格大小設置為400×400,它占據(jù)的平面區(qū)域是[0,1]×[0,1],所以網(wǎng)格的步長是dx = 1/400。采樣的最小間隔是每個小方格對角線的長度,即radius = sqrt(2)*dx。

我們把采樣點的目標個數(shù)設置為desired_examples=100000,這是一個目測值,因為400×400的網(wǎng)格包含160000個小方格,考慮到每個方格中至多只有一個點,我們能得到的滿足距離約束的點的最大數(shù)目肯定少于160000。

初始時網(wǎng)格中沒有任何點,所以需要將grid中的值都置為-1:

grid.fill(-1)

如何生成新的點

在加入新的隨機點時,我們總是從已有點的附近隨機選擇一個位置,然后比較它和已知點是否滿足最小距離約束,是的話就將其加入已有點,否則就將其拋棄然后重新選擇點。

這里需要注意的是:

  1. 當一個已有點附近已經(jīng)被填滿時,我們后面再加入新的點時就不必考慮它的附近了,可以用一個下標head來記錄這一點。我們約定samples數(shù)組中下標< head的點附近都已經(jīng)被填滿,從而不必再考慮它們,只考慮下標>= head的點即可。初始時head = 0。
  2. samples是一個長度為100K的數(shù)組,這不代表我們真的能取到這么多點,但具體個數(shù)是多少無法事先確定,所以我們還需要用一個下標tail來記錄目前已經(jīng)獲得的點的個數(shù)。初始時tail = 1,因為我們將選擇區(qū)域中心作為第一個點。當然這個初始點的位置可以是任意的。
  3. 正如前面提到的,當我們檢查一個點p是否與已有點滿足最小距離約束時,沒有必要遍歷檢查所有的點。只要檢查以p所在方格為中心,由5×5個方格組成的正方形區(qū)域即可。

檢查一個點是否和已有點沖突的邏輯我們單獨寫成一個函數(shù):

@ti.func
def check_collision(p, index):
    x, y = index
    collision = False
    for i in range(max(0, x - 2), min(grid_n, x + 3)):
        for j in range(max(0, y - 2), min(grid_n, y + 3)):
            if grid[i, j] != -1:
                q = samples[grid[i, j]]
                if (q - p).norm() < radius - 1e-6:
                    collision = True
    return collision

其中p是需要檢查點的坐標,index=(x, y)是p所在的方格的下標。

我們遍歷所有滿足x-2 <= i <= x+2和y-2 <= j <= y+2的下標(i, j),檢查方格(i, j)中是否已經(jīng)有點,即 grid[i, j]是否等于-1。有的話它與p的距離是否小于radius,然后返回對應的判斷。

完成了準備工作,我們可以開始正式的循環(huán)了:

@ti.kernel
def poisson_disk_sample(desired_samples: int) -> int:
    samples[0] = tm.vec2(0.5)
    grid[int(grid_n * 0.5), int(grid_n * 0.5)] = 0
    head, tail = 0, 1
    while head < tail and head < desired_samples:
        source_x = samples[head]
        head += 1

        for _ in range(100):
            theta = ti.random() * 2 * tm.pi
            offset = tm.vec2(tm.cos(theta), tm.sin(theta)) * (1 + ti.random()) * radius
            new_x = source_x + offset
            new_index = int(new_x * inv_dx)

            if 0 <= new_x[0] < 1 and 0 <= new_x[1] < 1:
                collision = check_collision(new_x, new_index)
                if not collision and tail < desired_samples:
                    samples[tail] = new_x
                    grid[new_index] = tail
                    tail += 1
    return tail

首先我們把區(qū)域的中心,即坐標為(0.5,0.5)的點選擇為初始點,讓它作為“種子”將隨機點逐漸擴散到整個區(qū)域。

接下來的while循環(huán)是算法的主體,這個循環(huán)是串行執(zhí)行的,只占用一個線程。

我們每次找到第一個需要考慮的點samples[head],然后在以它為中心,半徑為[radius, 2*raidus]的圓環(huán)中隨機選擇100個點,逐個檢查這100個點是否不超出[0,1]×[0,1]的區(qū)域范圍,以及是否和已有點沖突。

如果都滿足的話它就是一個合格的點,我們將它的坐標和方格下標更新到samples和grid中,并將已有點的個數(shù)tail增加1。在這100個點都檢查完后,可能有多個點會被加入已有點的集合。

注意在半徑為[radius, 2*raidus]的圓環(huán)中采樣可以讓我們得到的點在滿足最小距離約束的同時距離已有點也不會太遠。

當這100個點都檢查完畢后,我們可以認為samples[head]這個點的周圍已經(jīng)沒有空白區(qū)域可以放置新的點,所以將head增加1,并重新檢查下一個samples[head] 附近的區(qū)域。

當所有的點周圍的空間都已經(jīng)被填滿,即head = tail時,或者我們已經(jīng)獲得了desired_samples個點,即tail = desired_samples時循環(huán)結束。這時samples中下標在0~tail-1范圍內的點就是全部的已有點。

展示動畫效果

我們可以只用幾行代碼,就把整個采樣的過程用動畫的形式顯示出來:

num_samples = poisson_disk_sample(desired_samples)
gui = ti.GUI("Poisson Disk Sampling", res=800, background_color=0xFFFFFF)
count = 0
speed = 300
while gui.running:
    gui.circles(samples.to_numpy()[:min(count * speed, num_samples)],
                color=0x000000,
                radius=1.5)
    count += 1
    gui.show()

這里我們控制動畫的速度為每生成300個點就繪制一幀。

至此我們已經(jīng)介紹完了程序的所有要點,把各部分組合起來:

import taichi as ti
import taichi.math as tm
ti.init(arch=ti.cpu)

grid_n = 400
res = (grid_n, grid_n)
dx = 1 / res[0]
inv_dx = res[0]
radius = dx * ti.sqrt(2)
desired_samples = 100000
grid = ti.field(dtype=int, shape=res)
samples = ti.Vector.field(2, float, shape=desired_samples)

grid.fill(-1)

@ti.func
def check_collision(p, index):
    x, y = index
    collision = False
    for i in range(max(0, x - 2), min(grid_n, x + 3)):
        for j in range(max(0, y - 2), min(grid_n, y + 3)):
            if grid[i, j] != -1:
                q = samples[grid[i, j]]
                if (q - p).norm() < radius - 1e-6:
                    collision = True
    return collision

@ti.kernel
def poisson_disk_sample(desired_samples: int) -> int:
    samples[0] = tm.vec2(0.5)
    grid[int(grid_n * 0.5), int(grid_n * 0.5)] = 0
    head, tail = 0, 1
    while head < tail and head < desired_samples:
        source_x = samples[head]
        head += 1

        for _ in range(100):
            theta = ti.random() * 2 * tm.pi
            offset = tm.vec2(tm.cos(theta), tm.sin(theta)) * (1 + ti.random()) * radius
            new_x = source_x + offset
            new_index = int(new_x * inv_dx)

            if 0 <= new_x[0] < 1 and 0 <= new_x[1] < 1:
                collision = check_collision(new_x, new_index)
                if not collision and tail < desired_samples:
                    samples[tail] = new_x
                    grid[new_index] = tail
                    tail += 1
    return tail

num_samples = poisson_disk_sample(desired_samples)
gui = ti.GUI("Poisson Disk Sampling", res=800, background_color=0xFFFFFF)
count = 0
speed = 300
while gui.running:
    gui.circles(samples.to_numpy()[:min(count * speed, num_samples)],
                color=0x000000,
                radius=1.5)
    count += 1
    gui.show()

代碼總行數(shù):60。

One More Thing

具體來說,這篇代碼實現(xiàn)了兩個操作

  1. 60行代碼中實現(xiàn)了一個完整的泊松采樣動畫。
  2. 在一個400×400的網(wǎng)格中采集了53k個點,但耗時不到1秒。

相關代碼可以在文末的原文鏈接中找到。

嚴格來說,本文實現(xiàn)的算法和Bridson論文里描述的算法有一點點不一樣的地方(更簡單一些),但是效果卻差不多。

你能看出是哪里不一樣嗎?(TIP:可以關注一下原論文Step 2中“active list”的處理方式)

項目地址:
https://github.com/taichi-dev/poisson-sampling-homework

參考資料:
[1]Robert Bridson的原論文見Fast Poisson Disk Sampling in Arbitrary Dimensions.
[2]Poisson采樣用Taichi, Numpy, Numba實現(xiàn)的benchmark比較見GitHub

— 完 —

量子位 QbitAI · 頭條號簽約

關注我們,第一時間獲知前沿科技動態(tài)

]]>