十年網(wǎng)站開發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊
量身定制 + 運(yùn)營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
想象一下,您有一個線性方程組和不等式系統(tǒng)。這樣的系統(tǒng)通常有許多可能的解決方案。線性規(guī)劃是一組數(shù)學(xué)和計算工具,可讓您找到該系統(tǒng)的特定解,該解對應(yīng)于某些其他線性函數(shù)的最大值或最小值。
站在用戶的角度思考問題,與客戶深入溝通,找到盤錦網(wǎng)站設(shè)計與盤錦網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗(yàn)好的作品,建站類型包括:網(wǎng)站設(shè)計制作、做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、域名注冊、雅安服務(wù)器托管、企業(yè)郵箱。業(yè)務(wù)覆蓋盤錦地區(qū)。
混合整數(shù)線性規(guī)劃是 線性規(guī)劃 的擴(kuò)展。它處理至少一個變量采用離散整數(shù)而不是連續(xù)值的問題。盡管乍一看混合整數(shù)問題與連續(xù)變量問題相似,但它們在靈活性和精度方面具有顯著優(yōu)勢。
整數(shù)變量對于正確表示自然用整數(shù)表示的數(shù)量很重要,例如生產(chǎn)的飛機(jī)數(shù)量或服務(wù)的客戶數(shù)量。
一種特別重要的整數(shù)變量是 二進(jìn)制變量 。它只能取 零 或 一 的值,在做出是或否的決定時很有用,例如是否應(yīng)該建造工廠或者是否應(yīng)該打開或關(guān)閉機(jī)器。您還可以使用它們來模擬邏輯約束。
線性規(guī)劃是一種基本的優(yōu)化技術(shù),已在科學(xué)和數(shù)學(xué)密集型領(lǐng)域使用了數(shù)十年。它精確、相對快速,適用于一系列實(shí)際應(yīng)用。
混合整數(shù)線性規(guī)劃允許您克服線性規(guī)劃的許多限制。您可以使用分段線性函數(shù)近似非線性函數(shù)、使用半連續(xù)變量、模型邏輯約束等。它是一種計算密集型工具,但計算機(jī)硬件和軟件的進(jìn)步使其每天都更加適用。
通常,當(dāng)人們試圖制定和解決優(yōu)化問題時,第一個問題是他們是否可以應(yīng)用線性規(guī)劃或混合整數(shù)線性規(guī)劃。
以下文章說明了線性規(guī)劃和混合整數(shù)線性規(guī)劃的一些用例:
隨著計算機(jī)能力的增強(qiáng)、算法的改進(jìn)以及更多用戶友好的軟件解決方案的出現(xiàn),線性規(guī)劃,尤其是混合整數(shù)線性規(guī)劃的重要性隨著時間的推移而增加。
解決線性規(guī)劃問題的基本方法稱為,它有多種變體。另一種流行的方法是。
混合整數(shù)線性規(guī)劃問題可以通過更復(fù)雜且計算量更大的方法來解決,例如,它在幕后使用線性規(guī)劃。這種方法的一些變體是,它涉及使用 切割平面 ,以及。
有幾種適用于線性規(guī)劃和混合整數(shù)線性規(guī)劃的合適且眾所周知的 Python 工具。其中一些是開源的,而另一些是專有的。您是否需要免費(fèi)或付費(fèi)工具取決于問題的規(guī)模和復(fù)雜性,以及對速度和靈活性的需求。
值得一提的是,幾乎所有廣泛使用的線性規(guī)劃和混合整數(shù)線性規(guī)劃庫都是以 Fortran 或 C 或 C++ 原生和編寫的。這是因?yàn)榫€性規(guī)劃需要對(通常很大)矩陣進(jìn)行計算密集型工作。此類庫稱為求解器。Python 工具只是求解器的包裝器。
Python 適合圍繞本機(jī)庫構(gòu)建包裝器,因?yàn)樗梢院芎玫嘏c C/C++ 配合使用。對于本教程,您不需要任何 C/C++(或 Fortran),但如果您想了解有關(guān)此酷功能的更多信息,請查看以下資源:
基本上,當(dāng)您定義和求解模型時,您使用 Python 函數(shù)或方法調(diào)用低級庫,該庫執(zhí)行實(shí)際優(yōu)化工作并將解決方案返回給您的 Python 對象。
幾個免費(fèi)的 Python 庫專門用于與線性或混合整數(shù)線性規(guī)劃求解器交互:
在本教程中,您將使用SciPy和PuLP來定義和解決線性規(guī)劃問題。
在本節(jié)中,您將看到線性規(guī)劃問題的兩個示例:
您將在下一節(jié)中使用 Python 來解決這兩個問題。
考慮以下線性規(guī)劃問題:
你需要找到X和?使得紅色,藍(lán)色和黃色的不平等,以及不平等X 0和? 0,是滿意的。同時,您的解決方案必須對應(yīng)于z的最大可能值。
您需要找到的自變量(在本例中為 x 和 y )稱為 決策變量 。要最大化或最小化的決策變量的函數(shù)(在本例中為 z) 稱為 目標(biāo)函數(shù) 、 成本函數(shù) 或僅稱為 目標(biāo) 。您需要滿足的 不等式 稱為 不等式約束 。您還可以在稱為 等式約束 的約束中使用方程。
這是您如何可視化問題的方法:
紅線代表的功能2 X + Y = 20,和它上面的紅色區(qū)域示出了紅色不等式不滿足。同樣,藍(lán)線是函數(shù) 4 x + 5 y = 10,藍(lán)色區(qū)域被禁止,因?yàn)樗`反了藍(lán)色不等式。黃線是 x + 2 y = 2,其下方的黃色區(qū)域是黃色不等式無效的地方。
如果您忽略紅色、藍(lán)色和黃色區(qū)域,則僅保留灰色區(qū)域。灰色區(qū)域的每個點(diǎn)都滿足所有約束,是問題的潛在解決方案。該區(qū)域稱為 可行域 ,其點(diǎn)為 可行解 。在這種情況下,有無數(shù)可行的解決方案。
您想最大化z。對應(yīng)于最大z的可行解是 最優(yōu)解 。如果您嘗試最小化目標(biāo)函數(shù),那么最佳解決方案將對應(yīng)于其可行的最小值。
請注意,z是線性的。你可以把它想象成一個三維空間中的平面。這就是為什么最優(yōu)解必須在可行區(qū)域的 頂點(diǎn) 或角上的原因。在這種情況下,最佳解決方案是紅線和藍(lán)線相交的點(diǎn),稍后您將看到。
有時,可行區(qū)域的整個邊緣,甚至整個區(qū)域,都可以對應(yīng)相同的z值。在這種情況下,您有許多最佳解決方案。
您現(xiàn)在已準(zhǔn)備好使用綠色顯示的附加等式約束來擴(kuò)展問題:
方程式 x + 5 y = 15,以綠色書寫,是新的。這是一個等式約束。您可以通過向上一張圖像添加相應(yīng)的綠線來將其可視化:
現(xiàn)在的解決方案必須滿足綠色等式,因此可行區(qū)域不再是整個灰色區(qū)域。它是綠線從與藍(lán)線的交點(diǎn)到與紅線的交點(diǎn)穿過灰色區(qū)域的部分。后一點(diǎn)是解決方案。
如果插入x的所有值都必須是整數(shù)的要求,那么就會得到一個混合整數(shù)線性規(guī)劃問題,可行解的集合又會發(fā)生變化:
您不再有綠線,只有沿線的x值為整數(shù)的點(diǎn)??尚薪馐腔疑尘吧系木G點(diǎn),此時最優(yōu)解離紅線最近。
這三個例子說明了 可行的線性規(guī)劃問題 ,因?yàn)樗鼈兙哂杏薪缈尚袇^(qū)域和有限解。
如果沒有解,線性規(guī)劃問題是 不可行的 。當(dāng)沒有解決方案可以同時滿足所有約束時,通常會發(fā)生這種情況。
例如,考慮如果添加約束x + y 1會發(fā)生什么。那么至少有一個決策變量(x或y)必須是負(fù)數(shù)。這與給定的約束x 0 和y 0相沖突。這樣的系統(tǒng)沒有可行的解決方案,因此稱為不可行的。
另一個示例是添加與綠線平行的第二個等式約束。這兩行沒有共同點(diǎn),因此不會有滿足這兩個約束的解決方案。
一個線性規(guī)劃問題是 無界的 ,如果它的可行區(qū)域是無界,將溶液不是有限。這意味著您的變量中至少有一個不受約束,可以達(dá)到正無窮大或負(fù)無窮大,從而使目標(biāo)也無限大。
例如,假設(shè)您采用上面的初始問題并刪除紅色和黃色約束。從問題中刪除約束稱為 放松 問題。在這種情況下,x和y不會在正側(cè)有界。您可以將它們增加到正無窮大,從而產(chǎn)生無限大的z值。
在前面的部分中,您研究了一個與任何實(shí)際應(yīng)用程序無關(guān)的抽象線性規(guī)劃問題。在本小節(jié)中,您將找到與制造業(yè)資源分配相關(guān)的更具體和實(shí)用的優(yōu)化問題。
假設(shè)一家工廠生產(chǎn)四種不同的產(chǎn)品,第一種產(chǎn)品的日產(chǎn)量為x ?,第二種產(chǎn)品的產(chǎn)量為x 2,依此類推。目標(biāo)是確定每種產(chǎn)品的利潤最大化日產(chǎn)量,同時牢記以下條件:
數(shù)學(xué)模型可以這樣定義:
目標(biāo)函數(shù)(利潤)在條件 1 中定義。人力約束遵循條件 2。對原材料 A 和 B 的約束可以從條件 3 和條件 4 中通過對每種產(chǎn)品的原材料需求求和得出。
最后,產(chǎn)品數(shù)量不能為負(fù),因此所有決策變量必須大于或等于零。
與前面的示例不同,您無法方便地將其可視化,因?yàn)樗兴膫€決策變量。但是,無論問題的維度如何,原理都是相同的。
在本教程中,您將使用兩個Python 包來解決上述線性規(guī)劃問題:
SciPy 設(shè)置起來很簡單。安裝后,您將擁有開始所需的一切。它的子包 scipy.optimize 可用于線性和非線性優(yōu)化。
PuLP 允許您選擇求解器并以更自然的方式表述問題。PuLP 使用的默認(rèn)求解器是COIN-OR Branch and Cut Solver (CBC)。它連接到用于線性松弛的COIN-OR 線性規(guī)劃求解器 (CLP)和用于切割生成的COIN-OR 切割生成器庫 (CGL)。
另一個偉大的開源求解器是GNU 線性規(guī)劃工具包 (GLPK)。一些著名且非常強(qiáng)大的商業(yè)和專有解決方案是Gurobi、CPLEX和XPRESS。
除了在定義問題時提供靈活性和運(yùn)行各種求解器的能力外,PuLP 使用起來不如 Pyomo 或 CVXOPT 等替代方案復(fù)雜,后者需要更多的時間和精力來掌握。
要學(xué)習(xí)本教程,您需要安裝 SciPy 和 PuLP。下面的示例使用 SciPy 1.4.1 版和 PuLP 2.1 版。
您可以使用pip以下方法安裝兩者:
您可能需要運(yùn)行pulptest或sudo pulptest啟用 PuLP 的默認(rèn)求解器,尤其是在您使用 Linux 或 Mac 時:
或者,您可以下載、安裝和使用 GLPK。它是免費(fèi)和開源的,適用于 Windows、MacOS 和 Linux。在本教程的后面部分,您將看到如何將 GLPK(除了 CBC)與 PuLP 一起使用。
在 Windows 上,您可以下載檔案并運(yùn)行安裝文件。
在 MacOS 上,您可以使用 Homebrew:
在 Debian 和 Ubuntu 上,使用apt來安裝glpk和glpk-utils:
在Fedora,使用dnf具有g(shù)lpk-utils:
您可能還會發(fā)現(xiàn)conda對安裝 GLPK 很有用:
安裝完成后,可以查看GLPK的版本:
有關(guān)詳細(xì)信息,請參閱 GLPK 關(guān)于使用Windows 可執(zhí)行文件和Linux 軟件包進(jìn)行安裝的教程。
在本節(jié)中,您將學(xué)習(xí)如何使用 SciPy優(yōu)化和求根庫進(jìn)行線性規(guī)劃。
要使用 SciPy 定義和解決優(yōu)化問題,您需要導(dǎo)入scipy.optimize.linprog():
現(xiàn)在您已經(jīng)linprog()導(dǎo)入,您可以開始優(yōu)化。
讓我們首先解決上面的線性規(guī)劃問題:
linprog()僅解決最小化(而非最大化)問題,并且不允許具有大于或等于符號 ( ) 的不等式約束。要解決這些問題,您需要在開始優(yōu)化之前修改您的問題:
引入這些更改后,您將獲得一個新系統(tǒng):
該系統(tǒng)與原始系統(tǒng)等效,并且將具有相同的解決方案。應(yīng)用這些更改的唯一原因是克服 SciPy 與問題表述相關(guān)的局限性。
下一步是定義輸入值:
您將上述系統(tǒng)中的值放入適當(dāng)?shù)牧斜?、元組或NumPy 數(shù)組中:
注意:請注意行和列的順序!
約束左側(cè)和右側(cè)的行順序必須相同。每一行代表一個約束。
來自目標(biāo)函數(shù)和約束左側(cè)的系數(shù)的順序必須匹配。每列對應(yīng)一個決策變量。
下一步是以與系數(shù)相同的順序定義每個變量的界限。在這種情況下,它們都在零和正無窮大之間:
此語句是多余的,因?yàn)閘inprog()默認(rèn)情況下采用這些邊界(零到正無窮大)。
注:相反的float("inf"),你可以使用math.inf,numpy.inf或scipy.inf。
最后,是時候優(yōu)化和解決您感興趣的問題了。你可以這樣做linprog():
參數(shù)c是指來自目標(biāo)函數(shù)的系數(shù)。A_ub和b_ub分別與不等式約束左邊和右邊的系數(shù)有關(guān)。同樣,A_eq并b_eq參考等式約束。您可以使用bounds提供決策變量的下限和上限。
您可以使用該參數(shù)method來定義要使用的線性規(guī)劃方法。有以下三種選擇:
linprog() 返回具有以下屬性的數(shù)據(jù)結(jié)構(gòu):
您可以分別訪問這些值:
這就是您獲得優(yōu)化結(jié)果的方式。您還可以以圖形方式顯示它們:
如前所述,線性規(guī)劃問題的最優(yōu)解位于可行區(qū)域的頂點(diǎn)。在這種情況下,可行區(qū)域只是藍(lán)線和紅線之間的綠線部分。最優(yōu)解是代表綠線和紅線交點(diǎn)的綠色方塊。
如果要排除相等(綠色)約束,只需刪除參數(shù)A_eq并b_eq從linprog()調(diào)用中刪除:
解決方案與前一種情況不同。你可以在圖表上看到:
在這個例子中,最優(yōu)解是紅色和藍(lán)色約束相交的可行(灰色)區(qū)域的紫色頂點(diǎn)。其他頂點(diǎn),如黃色頂點(diǎn),具有更高的目標(biāo)函數(shù)值。
您可以使用 SciPy 來解決前面部分所述的資源分配問題:
和前面的例子一樣,你需要從上面的問題中提取必要的向量和矩陣,將它們作為參數(shù)傳遞給.linprog(),然后得到結(jié)果:
結(jié)果告訴您最大利潤是1900并且對應(yīng)于x ? = 5 和x ? = 45。在給定條件下生產(chǎn)第二和第四個產(chǎn)品是沒有利潤的。您可以在這里得出幾個有趣的結(jié)論:
opt.statusis0和opt.successis True,說明優(yōu)化問題成功求解,最優(yōu)可行解。
SciPy 的線性規(guī)劃功能主要用于較小的問題。對于更大和更復(fù)雜的問題,您可能會發(fā)現(xiàn)其他庫更適合,原因如下:
幸運(yùn)的是,Python 生態(tài)系統(tǒng)為線性編程提供了幾種替代解決方案,這些解決方案對于更大的問題非常有用。其中之一是 PuLP,您將在下一節(jié)中看到它的實(shí)際應(yīng)用。
PuLP 具有比 SciPy 更方便的線性編程 API。您不必在數(shù)學(xué)上修改您的問題或使用向量和矩陣。一切都更干凈,更不容易出錯。
像往常一樣,您首先導(dǎo)入您需要的內(nèi)容:
現(xiàn)在您已經(jīng)導(dǎo)入了 PuLP,您可以解決您的問題。
您現(xiàn)在將使用 PuLP 解決此系統(tǒng):
第一步是初始化一個實(shí)例LpProblem來表示你的模型:
您可以使用該sense參數(shù)來選擇是執(zhí)行最小化(LpMinimize或1,這是默認(rèn)值)還是最大化(LpMaximize或-1)。這個選擇會影響你的問題的結(jié)果。
一旦有了模型,就可以將決策變量定義為LpVariable類的實(shí)例:
您需要提供下限,lowBound=0因?yàn)槟J(rèn)值為負(fù)無窮大。該參數(shù)upBound定義了上限,但您可以在此處省略它,因?yàn)樗J(rèn)為正無窮大。
可選參數(shù)cat定義決策變量的類別。如果您使用的是連續(xù)變量,則可以使用默認(rèn)值"Continuous"。
您可以使用變量x和y創(chuàng)建表示線性表達(dá)式和約束的其他 PuLP 對象:
當(dāng)您將決策變量與標(biāo)量相乘或構(gòu)建多個決策變量的線性組合時,您會得到一個pulp.LpAffineExpression代表線性表達(dá)式的實(shí)例。
注意:您可以增加或減少變量或表達(dá)式,你可以乘他們常數(shù),因?yàn)榧垵{類實(shí)現(xiàn)一些Python的特殊方法,即模擬數(shù)字類型一樣__add__(),__sub__()和__mul__()。這些方法用于像定制運(yùn)營商的行為+,-和*。
類似地,您可以將線性表達(dá)式、變量和標(biāo)量與運(yùn)算符 ==、=以獲取表示模型線性約束的紙漿.LpConstraint實(shí)例。
注:也有可能與豐富的比較方法來構(gòu)建的約束.__eq__(),.__le__()以及.__ge__()定義了運(yùn)營商的行為==,=。
考慮到這一點(diǎn),下一步是創(chuàng)建約束和目標(biāo)函數(shù)并將它們分配給您的模型。您不需要創(chuàng)建列表或矩陣。只需編寫 Python 表達(dá)式并使用+=運(yùn)算符將它們附加到模型中:
在上面的代碼中,您定義了包含約束及其名稱的元組。LpProblem允許您通過將約束指定為元組來向模型添加約束。第一個元素是一個LpConstraint實(shí)例。第二個元素是該約束的可讀名稱。
設(shè)置目標(biāo)函數(shù)非常相似:
或者,您可以使用更短的符號:
現(xiàn)在您已經(jīng)添加了目標(biāo)函數(shù)并定義了模型。
注意:您可以使用運(yùn)算符將 約束或目標(biāo)附加到模型中,+=因?yàn)樗念怢pProblem實(shí)現(xiàn)了特殊方法.__iadd__(),該方法用于指定 的行為+=。
對于較大的問題,lpSum()與列表或其他序列一起使用通常比重復(fù)+運(yùn)算符更方便。例如,您可以使用以下語句將目標(biāo)函數(shù)添加到模型中:
它產(chǎn)生與前一條語句相同的結(jié)果。
您現(xiàn)在可以看到此模型的完整定義:
模型的字符串表示包含所有相關(guān)數(shù)據(jù):變量、約束、目標(biāo)及其名稱。
注意:字符串表示是通過定義特殊方法構(gòu)建的.__repr__()。有關(guān) 的更多詳細(xì)信息.__repr__(),請查看Pythonic OOP 字符串轉(zhuǎn)換:__repr__vs__str__ .
最后,您已準(zhǔn)備好解決問題。你可以通過調(diào)用.solve()你的模型對象來做到這一點(diǎn)。如果要使用默認(rèn)求解器 (CBC),則不需要傳遞任何參數(shù):
.solve()調(diào)用底層求解器,修改model對象,并返回解決方案的整數(shù)狀態(tài),1如果找到了最優(yōu)解。有關(guān)其余狀態(tài)代碼,請參閱LpStatus[]。
你可以得到優(yōu)化結(jié)果作為 的屬性model。該函數(shù)value()和相應(yīng)的方法.value()返回屬性的實(shí)際值:
model.objective持有目標(biāo)函數(shù)model.constraints的值,包含松弛變量的值,以及對象x和y具有決策變量的最優(yōu)值。model.variables()返回一個包含決策變量的列表:
如您所見,此列表包含使用 的構(gòu)造函數(shù)創(chuàng)建的確切對象LpVariable。
結(jié)果與您使用 SciPy 獲得的結(jié)果大致相同。
注意:注意這個方法.solve()——它會改變對象的狀態(tài),x并且y!
您可以通過調(diào)用查看使用了哪個求解器.solver:
輸出通知您求解器是 CBC。您沒有指定求解器,因此 PuLP 調(diào)用了默認(rèn)求解器。
如果要運(yùn)行不同的求解器,則可以將其指定為 的參數(shù).solve()。例如,如果您想使用 GLPK 并且已經(jīng)安裝了它,那么您可以solver=GLPK(msg=False)在最后一行使用。請記住,您還需要導(dǎo)入它:
現(xiàn)在你已經(jīng)導(dǎo)入了 GLPK,你可以在里面使用它.solve():
該msg參數(shù)用于顯示來自求解器的信息。msg=False禁用顯示此信息。如果要包含信息,則只需省略msg或設(shè)置msg=True。
您的模型已定義并求解,因此您可以按照與前一種情況相同的方式檢查結(jié)果:
使用 GLPK 得到的結(jié)果與使用 SciPy 和 CBC 得到的結(jié)果幾乎相同。
一起來看看這次用的是哪個求解器:
正如您在上面用突出顯示的語句定義的那樣model.solve(solver=GLPK(msg=False)),求解器是 GLPK。
您還可以使用 PuLP 來解決混合整數(shù)線性規(guī)劃問題。要定義整數(shù)或二進(jìn)制變量,只需傳遞cat="Integer"或cat="Binary"到LpVariable。其他一切都保持不變:
在本例中,您有一個整數(shù)變量并獲得與之前不同的結(jié)果:
Nowx是一個整數(shù),如模型中所指定。(從技術(shù)上講,它保存一個小數(shù)點(diǎn)后為零的浮點(diǎn)值。)這一事實(shí)改變了整個解決方案。讓我們在圖表上展示這一點(diǎn):
如您所見,最佳解決方案是灰色背景上最右邊的綠點(diǎn)。這是兩者的最大價值的可行的解決方案x和y,給它的最大目標(biāo)函數(shù)值。
GLPK 也能夠解決此類問題。
現(xiàn)在你可以使用 PuLP 來解決上面的資源分配問題:
定義和解決問題的方法與前面的示例相同:
在這種情況下,您使用字典 x來存儲所有決策變量。這種方法很方便,因?yàn)樽值淇梢詫Q策變量的名稱或索引存儲為鍵,將相應(yīng)的LpVariable對象存儲為值。列表或元組的LpVariable實(shí)例可以是有用的。
上面的代碼產(chǎn)生以下結(jié)果:
如您所見,該解決方案與使用 SciPy 獲得的解決方案一致。最有利可圖的解決方案是每天生產(chǎn)5.0第一件產(chǎn)品和45.0第三件產(chǎn)品。
讓我們把這個問題變得更復(fù)雜和有趣。假設(shè)由于機(jī)器問題,工廠無法同時生產(chǎn)第一種和第三種產(chǎn)品。在這種情況下,最有利可圖的解決方案是什么?
現(xiàn)在您有另一個邏輯約束:如果x ? 為正數(shù),則x ? 必須為零,反之亦然。這是二元決策變量非常有用的地方。您將使用兩個二元決策變量y ? 和y ?,它們將表示是否生成了第一個或第三個產(chǎn)品:
除了突出顯示的行之外,代碼與前面的示例非常相似。以下是差異:
這是解決方案:
事實(shí)證明,最佳方法是排除第一種產(chǎn)品而只生產(chǎn)第三種產(chǎn)品。
就像有許多資源可以幫助您學(xué)習(xí)線性規(guī)劃和混合整數(shù)線性規(guī)劃一樣,還有許多具有 Python 包裝器的求解器可用。這是部分列表:
其中一些庫,如 Gurobi,包括他們自己的 Python 包裝器。其他人使用外部包裝器。例如,您看到可以使用 PuLP 訪問 CBC 和 GLPK。
您現(xiàn)在知道什么是線性規(guī)劃以及如何使用 Python 解決線性規(guī)劃問題。您還了解到 Python 線性編程庫只是本機(jī)求解器的包裝器。當(dāng)求解器完成其工作時,包裝器返回解決方案狀態(tài)、決策變量值、松弛變量、目標(biāo)函數(shù)等。
最優(yōu)化
為什么要做最優(yōu)化呢?因?yàn)樵谏钪校藗兛偸窍M腋V祷蚱渌_(dá)到一個極值,比如做生意時希望成本最小,收入最大,所以在很多商業(yè)情境中,都會遇到求極值的情況。
函數(shù)求根
這里「函數(shù)的根」也稱「方程的根」,或「函數(shù)的零點(diǎn)」。
先把我們需要的包加載進(jìn)來。import numpy as npimport scipy as spimport scipy.optimize as optimport matplotlib.pyplot as plt%matplotlib inline
函數(shù)求根和最優(yōu)化的關(guān)系?什么時候函數(shù)是最小值或最大值?
兩個問題一起回答:最優(yōu)化就是求函數(shù)的最小值或最大值,同時也是極值,在求一個函數(shù)最小值或最大值時,它所在的位置肯定是導(dǎo)數(shù)為 0 的位置,所以要求一個函數(shù)的極值,必然要先求導(dǎo),使其為 0,所以函數(shù)求根就是為了得到最大值最小值。
scipy.optimize 有什么方法可以求根?
可以用 scipy.optimize 中的 bisect 或 brentq 求根。f = lambda x: np.cos(x) - x # 定義一個匿名函數(shù)x = np.linspace(-5, 5, 1000) # 先生成 1000 個 xy = f(x) # 對應(yīng)生成 1000 個 f(x)plt.plot(x, y); # 看一下這個函數(shù)長什么樣子plt.axhline(0, color='k'); # 畫一根橫線,位置在 y=0
opt.bisect(f, -5, 5) # 求取函數(shù)的根0.7390851332155535plt.plot(x, y)plt.axhline(0, color='k')plt.scatter([_], [0], c='r', s=100); # 這里的 [_] 表示上一個 Cell 中的結(jié)果,這里是 x 軸上的位置,0 是 y 上的位置
求根有兩種方法,除了上面介紹的 bisect,還有 brentq,后者比前者快很多。%timeit opt.bisect(f, -5, 5)%timeit opt.brentq(f, -5, 5)10000 loops, best of 3: 157 s per loopThe slowest run took 11.65 times longer than the fastest. This could mean that an intermediate result is being cached.10000 loops, best of 3: 35.9 s per loop
函數(shù)求最小化
求最小值就是一個最優(yōu)化問題。求最大值時只需對函數(shù)做一個轉(zhuǎn)換,比如加一個負(fù)號,或者取倒數(shù),就可轉(zhuǎn)成求最小值問題。所以兩者是同一問題。
初始值對最優(yōu)化的影響是什么?
舉例來說,先定義個函數(shù)。f = lambda x: 1-np.sin(x)/xx = np.linspace(-20., 20., 1000)y = f(x)
當(dāng)初始值為 3 值,使用 minimize 函數(shù)找到最小值。minimize 函數(shù)是在新版的 scipy 里,取代了以前的很多最優(yōu)化函數(shù),是個通用的接口,背后是很多方法在支撐。x0 = 3xmin = opt.minimize(f, x0).x # x0 是起始點(diǎn),起始點(diǎn)最好離真正的最小值點(diǎn)不要太遠(yuǎn)plt.plot(x, y)plt.scatter(x0, f(x0), marker='o', s=300); # 起始點(diǎn)畫出來,用圓圈表示plt.scatter(xmin, f(xmin), marker='v', s=300); # 最小值點(diǎn)畫出來,用三角表示plt.xlim(-20, 20);
初始值為 3 時,成功找到最小值。
現(xiàn)在來看看初始值為 10 時,找到的最小值點(diǎn)。x0 = 10xmin = opt.minimize(f, x0).xplt.plot(x, y)plt.scatter(x0, f(x0), marker='o', s=300)plt.scatter(xmin, f(xmin), marker='v', s=300)plt.xlim(-20, 20);
由上圖可見,當(dāng)初始值為 10 時,函數(shù)找到的是局部最小值點(diǎn),可見 minimize 的默認(rèn)算法對起始點(diǎn)的依賴性。
那么怎么才能不管初始值在哪個位置,都能找到全局最小值點(diǎn)呢?
如何找到全局最優(yōu)點(diǎn)?
可以使用 basinhopping 函數(shù)找到全局最優(yōu)點(diǎn),相關(guān)背后算法,可以看幫助文件,有提供論文的索引和出處。
我們設(shè)初始值為 10 看是否能找到全局最小值點(diǎn)。x0 = 10from scipy.optimize import basinhoppingxmin = basinhopping(f,x0,stepsize = 5).xplt.plot(x, y);plt.scatter(x0, f(x0), marker='o', s=300);plt.scatter(xmin, f(xmin), marker='v', s=300);plt.xlim(-20, 20);
當(dāng)起始點(diǎn)在比較遠(yuǎn)的位置,依然成功找到了全局最小值點(diǎn)。
如何求多元函數(shù)最小值?
以二元函數(shù)為例,使用 minimize 求對應(yīng)的最小值。def g(X): x,y = X return (x-1)**4 + 5 * (y-1)**2 - 2*x*yX_opt = opt.minimize(g, (8, 3)).x # (8,3) 是起始點(diǎn)print X_opt[ 1.88292611 1.37658521]fig, ax = plt.subplots(figsize=(6, 4)) # 定義畫布和圖形x_ = y_ = np.linspace(-1, 4, 100)X, Y = np.meshgrid(x_, y_)c = ax.contour(X, Y, g((X, Y)), 50) # 等高線圖ax.plot(X_opt[0], X_opt[1], 'r*', markersize=15) # 最小點(diǎn)的位置是個元組ax.set_xlabel(r"$x_1$", fontsize=18)ax.set_ylabel(r"$x_2$", fontsize=18)plt.colorbar(c, ax=ax) # colorbar 表示顏色越深,高度越高fig.tight_layout()
畫3D 圖。from mpl_toolkits.mplot3d import Axes3Dfrom matplotlib import cmfig = plt.figure()ax = fig.gca(projection='3d')x_ = y_ = np.linspace(-1, 4, 100)X, Y = np.meshgrid(x_, y_)surf = ax.plot_surface(X, Y, g((X,Y)), rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False)cset = ax.contour(X, Y, g((X,Y)), zdir='z',offset=-5, cmap=cm.coolwarm)fig.colorbar(surf, shrink=0.5, aspect=5);
曲線擬合
曲線擬合和最優(yōu)化有什么關(guān)系?
曲線擬合的問題是,給定一組數(shù)據(jù),它可能是沿著一條線散布的,這時要找到一條最優(yōu)的曲線來擬合這些數(shù)據(jù),也就是要找到最好的線來代表這些點(diǎn),這里的最優(yōu)是指這些點(diǎn)和線之間的距離是最小的,這就是為什么要用最優(yōu)化問題來解決曲線擬合問題。
舉例說明,給一些點(diǎn),找到一條線,來擬合這些點(diǎn)。
先給定一些點(diǎn):N = 50 # 點(diǎn)的個數(shù)m_true = 2 # 斜率b_true = -1 # 截距dy = 2.0 # 誤差np.random.seed(0)xdata = 10 * np.random.random(N) # 50 個 x,服從均勻分布ydata = np.random.normal(b_true + m_true * xdata, dy) # dy 是標(biāo)準(zhǔn)差plt.errorbar(xdata, ydata, dy, fmt='.k', ecolor='lightgray');
上面的點(diǎn)整體上呈現(xiàn)一個線性關(guān)系,要找到一條斜線來代表這些點(diǎn),這就是經(jīng)典的一元線性回歸。目標(biāo)就是找到最好的線,使點(diǎn)和線的距離最短。要優(yōu)化的函數(shù)是點(diǎn)和線之間的距離,使其最小。點(diǎn)是確定的,而線是可變的,線是由參數(shù)值,斜率和截距決定的,這里就是要通過優(yōu)化距離找到最優(yōu)的斜率和截距。
點(diǎn)和線的距離定義如下:def chi2(theta, x, y): return np.sum(((y - theta[0] - theta[1] * x)) ** 2)
上式就是誤差平方和。
誤差平方和是什么?有什么作用?
誤差平方和公式為:
誤差平方和大,表示真實(shí)的點(diǎn)和預(yù)測的線之間距離太遠(yuǎn),說明擬合得不好,最好的線,應(yīng)該是使誤差平方和最小,即最優(yōu)的擬合線,這里是條直線。
誤差平方和就是要最小化的目標(biāo)函數(shù)。
找到最優(yōu)的函數(shù),即斜率和截距。theta_guess = [0, 1] # 初始值theta_best = opt.minimize(chi2, theta_guess, args=(xdata, ydata)).xprint(theta_best)[-1.01442005 1.93854656]
上面兩個輸出即是預(yù)測的直線斜率和截距,我們是根據(jù)點(diǎn)來反推直線的斜率和截距,那么真實(shí)的斜率和截距是多少呢?-1 和 2,很接近了,差的一點(diǎn)是因?yàn)橛性胍舻囊搿fit = np.linspace(0, 10)yfit = theta_best[0] + theta_best[1] * xfitplt.errorbar(xdata, ydata, dy, fmt='.k', ecolor='lightgray');plt.plot(xfit, yfit, '-k');
最小二乘(Least Square)是什么?
上面用的是 minimize 方法,這個問題的目標(biāo)函數(shù)是誤差平方和,這就又有一個特定的解法,即最小二乘。
最小二乘的思想就是要使得觀測點(diǎn)和估計點(diǎn)的距離的平方和達(dá)到最小,這里的“二乘”指的是用平方來度量觀測點(diǎn)與估計點(diǎn)的遠(yuǎn)近(在古漢語中“平方”稱為“二乘”),“最小”指的是參數(shù)的估計值要保證各個觀測點(diǎn)與估計點(diǎn)的距離的平方和達(dá)到最小。
關(guān)于最小二乘估計的計算,涉及更多的數(shù)學(xué)知識,這里不想詳述,其一般的過程是用目標(biāo)函數(shù)對各參數(shù)求偏導(dǎo)數(shù),并令其等于 0,得到一個線性方程組。具體推導(dǎo)過程可參考斯坦福機(jī)器學(xué)習(xí)講義 第 7 頁。def deviations(theta, x, y): return (y - theta[0] - theta[1] * x)theta_best, ier = opt.leastsq(deviations, theta_guess, args=(xdata, ydata))print(theta_best)[-1.01442016 1.93854659]
最小二乘 leastsq 的結(jié)果跟 minimize 結(jié)果一樣。注意 leastsq 的第一個參數(shù)不再是誤差平方和 chi2,而是誤差本身 deviations,即沒有平方,也沒有和。yfit = theta_best[0] + theta_best[1] * xfitplt.errorbar(xdata, ydata, dy, fmt='.k', ecolor='lightgray');plt.plot(xfit, yfit, '-k');
非線性最小二乘
上面是給一些點(diǎn),擬合一條直線,擬合一條曲線也是一樣的。def f(x, beta0, beta1, beta2): # 首先定義一個非線性函數(shù),有 3 個參數(shù) return beta0 + beta1 * np.exp(-beta2 * x**2)beta = (0.25, 0.75, 0.5) # 先猜 3 個 betaxdata = np.linspace(0, 5, 50)y = f(xdata, *beta)ydata = y + 0.05 * np.random.randn(len(xdata)) # 給 y 加噪音def g(beta): return ydata - f(xdata, *beta) # 真實(shí) y 和 預(yù)測值的差,求最優(yōu)曲線時要用到beta_start = (1, 1, 1)beta_opt, beta_cov = opt.leastsq(g, beta_start)print beta_opt # 求到的 3 個最優(yōu)的 beta 值[ 0.25525709 0.74270226 0.54966466]
拿估計的 beta_opt 值跟真實(shí)的 beta = (0.25, 0.75, 0.5) 值比較,差不多。fig, ax = plt.subplots()ax.scatter(xdata, ydata) # 畫點(diǎn)ax.plot(xdata, y, 'r', lw=2) # 真實(shí)值的線ax.plot(xdata, f(xdata, *beta_opt), 'b', lw=2) # 擬合的線ax.set_xlim(0, 5)ax.set_xlabel(r"$x$", fontsize=18)ax.set_ylabel(r"$f(x, \beta)$", fontsize=18)fig.tight_layout()
除了使用最小二乘,還可以使用曲線擬合的方法,得到的結(jié)果是一樣的。beta_opt, beta_cov = opt.curve_fit(f, xdata, ydata)print beta_opt[ 0.25525709 0.74270226 0.54966466]
有約束的最小化
有約束的最小化是指,要求函數(shù)最小化之外,還要滿足約束條件,舉例說明。
邊界約束def f(X): x, y = X return (x-1)**2 + (y-1)**2 # 這是一個碗狀的函數(shù)x_opt = opt.minimize(f, (0, 0), method='BFGS').x # 無約束最優(yōu)化
假設(shè)有約束條件,x 和 y 要在一定的范圍內(nèi),如 x 在 2 到 3 之間,y 在 0 和 2 之間。bnd_x1, bnd_x2 = (2, 3), (0, 2) # 對自變量的約束x_cons_opt = opt.minimize(f, np.array([0, 0]), method='L-BFGS-B', bounds=[bnd_x1, bnd_x2]).x # bounds 矩形約束fig, ax = plt.subplots(figsize=(6, 4))x_ = y_ = np.linspace(-1, 3, 100)X, Y = np.meshgrid(x_, y_)c = ax.contour(X, Y, f((X,Y)), 50)ax.plot(x_opt[0], x_opt[1], 'b*', markersize=15) # 沒有約束下的最小值,藍(lán)色五角星ax.plot(x_cons_opt[0], x_cons_opt[1], 'r*', markersize=15) # 有約束下的最小值,紅色星星bound_rect = plt.Rectangle((bnd_x1[0], bnd_x2[0]), bnd_x1[1] - bnd_x1[0], bnd_x2[1] - bnd_x2[0], facecolor="grey")ax.add_patch(bound_rect)ax.set_xlabel(r"$x_1$", fontsize=18)ax.set_ylabel(r"$x_2$", fontsize=18)plt.colorbar(c, ax=ax)fig.tight_layout()
不等式約束
介紹下相關(guān)理論,先來看下存在等式約束的極值問題求法,比如下面的優(yōu)化問題。
目標(biāo)函數(shù)是 f(w),下面是等式約束,通常解法是引入拉格朗日算子,這里使用 ββ 來表示算子,得到拉格朗日公式為
l 是等式約束的個數(shù)。
然后分別對 w 和ββ 求偏導(dǎo),使得偏導(dǎo)數(shù)等于 0,然后解出 w 和βiβi,至于為什么引入拉格朗日算子可以求出極值,原因是 f(w) 的 dw 變化方向受其他不等式的約束,dw的變化方向與f(w)的梯度垂直時才能獲得極值,而且在極值處,f(w) 的梯度與其他等式梯度的線性組合平行,因此他們之間存在線性關(guān)系。(參考《最優(yōu)化與KKT條件》)
對于不等式約束的極值問題
常常利用拉格朗日對偶性將原始問題轉(zhuǎn)換為對偶問題,通過解對偶問題而得到原始問題的解。該方法應(yīng)用在許多統(tǒng)計學(xué)習(xí)方法中。有興趣的可以參閱相關(guān)資料,這里不再贅述。def f(X): return (X[0] - 1)**2 + (X[1] - 1)**2def g(X): return X[1] - 1.75 - (X[0] - 0.75)**4x_opt = opt.minimize(f, (0, 0), method='BFGS').xconstraints = [dict(type='ineq', fun=g)] # 約束采用字典定義,約束方式為不等式約束,邊界用 g 表示x_cons_opt = opt.minimize(f, (0, 0), method='SLSQP', constraints=constraints).xfig, ax = plt.subplots(figsize=(6, 4))x_ = y_ = np.linspace(-1, 3, 100)X, Y = np.meshgrid(x_, y_)c = ax.contour(X, Y, f((X, Y)), 50)ax.plot(x_opt[0], x_opt[1], 'b*', markersize=15) # 藍(lán)色星星,沒有約束下的最小值ax.plot(x_, 1.75 + (x_-0.75)**4, '', markersize=15)ax.fill_between(x_, 1.75 + (x_-0.75)**4, 3, color="grey")ax.plot(x_cons_opt[0], x_cons_opt[1], 'r*', markersize=15) # 在區(qū)域約束下的最小值ax.set_ylim(-1, 3)ax.set_xlabel(r"$x_0$", fontsize=18)ax.set_ylabel(r"$x_1$", fontsize=18)plt.colorbar(c, ax=ax)fig.tight_layout()
scipy.optimize.minimize 中包括了多種最優(yōu)化算法,每種算法使用范圍不同,詳細(xì)參考官方文檔。
是否非常想學(xué)好 Python,一方面被瑣事糾纏,一直沒能動手,另一方面,擔(dān)心學(xué)習(xí)成本太高,心里默默敲著退堂鼓?
幸運(yùn)的是,Python 是一門初學(xué)者友好的編程語言,想要完全掌握它,你不必花上太多的時間和精力。
Python 的設(shè)計哲學(xué)之一就是簡單易學(xué),體現(xiàn)在兩個方面:
語法簡潔明了:相對 Ruby 和 Perl,它的語法特性不多不少,大多數(shù)都很簡單直接,不玩兒玄學(xué)。
切入點(diǎn)很多:Python 可以讓你可以做很多事情,科學(xué)計算和數(shù)據(jù)分析、爬蟲、Web 網(wǎng)站、游戲、命令行實(shí)用工具等等等等,總有一個是你感興趣并且愿意投入時間的。
廢話不多說,學(xué)會一門語言的捷徑只有一個: Getting Started
? 起步階段
任何一種編程語言都包含兩個部分:硬知識和軟知識,起步階段的主要任務(wù)是掌握硬知識。
硬知識
“硬知識”指的是編程語言的語法、算法和數(shù)據(jù)結(jié)構(gòu)、編程范式等,例如:變量和類型、循環(huán)語句、分支、函數(shù)、類。這部分知識也是具有普適性的,看上去是掌握了一種語法,實(shí)際是建立了一種思維。例如:讓一個 Java 程序員去學(xué)習(xí) Python,他可以很快的將 Java 中的學(xué)到的面向?qū)ο蟮闹R map 到 Python 中來,因此能夠快速掌握 Python 中面向?qū)ο蟮奶匦浴?/p>
如果你是剛開始學(xué)習(xí)編程的新手,一本可靠的語法書是非常重要的。它看上去可能非??菰锓ξ?,但對于建立穩(wěn)固的編程思維是必不可少。
下面列出了一些適合初學(xué)者入門的教學(xué)材料:
廖雪峰的 Python 教程 ? ?Python 中文教程的翹楚,專為剛剛步入程序世界的小白打造。 ?
笨方法學(xué) Python ? ?這本書在講解 Python 的語法成分時,還附帶大量可實(shí)踐的例子,非常適合快速起步。 ?
The Hitchhiker’s Guide to Python! ? ?這本指南著重于 Python 的最佳實(shí)踐,不管你是 Python 專家還是新手,都能獲得極大的幫助。 ?
Python 的哲學(xué):
用一種方法,最好是只有一種方法來做一件事。
學(xué)習(xí)也是一樣,雖然推薦了多種學(xué)習(xí)資料,但實(shí)際學(xué)習(xí)的時候,最好只選擇其中的一個,堅持看完。
必要的時候,可能需要閱讀講解數(shù)據(jù)結(jié)構(gòu)和算法的書,這些知識對于理解和使用 Python 中的對象模型有著很大的幫助。
軟知識
“軟知識”則是特定語言環(huán)境下的語法技巧、類庫的使用、IDE的選擇等等。這一部分,即使完全不了解不會使用,也不會妨礙你去編程,只不過寫出的程序,看上去顯得“傻”了些。
對這些知識的學(xué)習(xí),取決于你嘗試解決的問題的領(lǐng)域和深度。對初學(xué)者而言,起步階段極易走火,或者在選擇 Python 版本時徘徊不決,一會兒看 2.7 一會兒又轉(zhuǎn)到 3.0,或者徜徉在類庫的大海中無法自拔,Scrapy,Numpy,Django 什么都要試試,或者參與編輯器圣戰(zhàn)、大括號縮進(jìn)探究、操作系統(tǒng)辯論賽等無意義活動,或者整天跪舔語法糖,老想著怎么一行代碼把所有的事情做完,或者去構(gòu)想圣潔的性能安全通用性健壯性全部滿分的解決方案。
很多“大?!倍紩嬲]初學(xué)者,用這個用那個,少走彎路,這樣反而把初學(xué)者推向了真正的彎路。
還不如告訴初學(xué)者,學(xué)習(xí)本來就是個需要你去走彎路出 Bug,只能腳踏實(shí)地,沒有奇跡只有狗屎的過程。
選擇一個方向先走下去,哪怕臟丑差,走不動了再看看有沒有更好的解決途徑。
自己走了彎路,你才知道這么做的好處,才能理解為什么人們可以手寫狀態(tài)機(jī)去匹配卻偏要發(fā)明正則表達(dá)式,為什么面向過程可以解決卻偏要面向?qū)ο螅瑸槭裁次铱梢圆倏v每一根指針卻偏要自動管理內(nèi)存,為什么我可以嵌套回調(diào)卻偏要用 Promise...
更重要的是,你會明白,高層次的解決方法都是對低層次的封裝,并不是任何情況下都是最有效最合適的。
技術(shù)涌進(jìn)就像波浪一樣,那些陳舊的封存已久的技術(shù),消退了遲早還會涌回的。就像現(xiàn)在移動端應(yīng)用、手游和 HTML5 的火熱,某些方面不正在重演過去 PC 的那些歷史么?
因此,不要擔(dān)心自己走錯路誤了終身,堅持并保持進(jìn)步才是正道。
起步階段的核心任務(wù)是掌握硬知識,軟知識做適當(dāng)了解,有了穩(wěn)固的根,粗壯的枝干,才能長出濃密的葉子,結(jié)出甜美的果實(shí)。
? 發(fā)展階段
完成了基礎(chǔ)知識的學(xué)習(xí),必定會感到一陣空虛,懷疑這些語法知識是不是真的有用。
沒錯,你的懷疑是非常正確的。要讓 Python 發(fā)揮出它的價值,當(dāng)然不能停留在語法層面。
發(fā)展階段的核心任務(wù),就是“跳出 Python,擁抱世界”。
在你面前會有多個分支:科學(xué)計算和數(shù)據(jù)分析、爬蟲、Web 網(wǎng)站、游戲、命令行實(shí)用工具等等等等,這些都不是僅僅知道 Python 語法就能解決的問題。
拿爬蟲舉例,如果你對計算機(jī)網(wǎng)絡(luò),HTTP 協(xié)議,HTML,文本編碼,JSON 一無所知,你能做好這部分的工作么?而你在起步階段的基礎(chǔ)知識也同樣重要,如果你連循環(huán)遞歸怎么寫都還要查文檔,連 BFS 都不知道怎么實(shí)現(xiàn),這就像工匠做石凳每次起錘都要思考錘子怎么使用一樣,非常低效。
在這個階段,不可避免要接觸大量類庫,閱讀大量書籍的。
類庫方面
「Awesome Python 項(xiàng)目」:vinta/awesome-python · GitHub
這里列出了你在嘗試解決各種實(shí)際問題時,Python 社區(qū)已有的工具型類庫,如下圖所示:
請點(diǎn)擊輸入圖片描述
vinta/awesome-python
你可以按照實(shí)際需求,尋找你需要的類庫。
至于相關(guān)類庫如何使用,必須掌握的技能便是閱讀文檔。由于開源社區(qū)大多數(shù)文檔都是英文寫成的,所以,英語不好的同學(xué),需要惡補(bǔ)下。
書籍方面
這里我只列出一些我覺得比較有一些幫助的書籍,詳細(xì)的請看豆瓣的書評:
科學(xué)和數(shù)據(jù)分析:
?「集體智慧編程」:集體智慧編程 (豆瓣)
?「數(shù)學(xué)之美」:數(shù)學(xué)之美 (豆瓣)
?「統(tǒng)計學(xué)習(xí)方法」:統(tǒng)計學(xué)習(xí)方法 (豆瓣)
?「Pattern Recognition And Machine Learning」:Pattern Recognition And Machine Learning (豆瓣)
?「數(shù)據(jù)科學(xué)實(shí)戰(zhàn)」:數(shù)據(jù)科學(xué)實(shí)戰(zhàn) (豆瓣)
?「數(shù)據(jù)檢索導(dǎo)論」:信息檢索導(dǎo)論 (豆瓣)
爬蟲:
?「HTTP 權(quán)威指南」:HTTP權(quán)威指南 (豆瓣)
Web 網(wǎng)站:
?「HTML CSS 設(shè)計與構(gòu)建網(wǎng)站」:HTML CSS設(shè)計與構(gòu)建網(wǎng)站 (豆瓣)
...
列到這里已經(jīng)不需要繼續(xù)了。
聰明的你一定會發(fā)現(xiàn)上面的大部分書籍,并不是講 Python 的書,而更多的是專業(yè)知識。
事實(shí)上,這里所謂“跳出 Python,擁抱世界”,其實(shí)是發(fā)現(xiàn) Python 和專業(yè)知識相結(jié)合,能夠解決很多實(shí)際問題。這個階段能走到什么程度,更多的取決于自己的專業(yè)知識。
? 深入階段
這個階段的你,對 Python 幾乎了如指掌,那么你一定知道 Python 是用 C 語言實(shí)現(xiàn)的。
可是 Python 對象的“動態(tài)特征”是怎么用相對底層,連自動內(nèi)存管理都沒有的C語言實(shí)現(xiàn)的呢?這時候就不能停留在表面了,勇敢的拆開 Python 的黑盒子,深入到語言的內(nèi)部,去看它的歷史,讀它的源碼,才能真正理解它的設(shè)計思路。
這里推薦一本書:
「Python 源碼剖析」:Python源碼剖析 (豆瓣)
這本書把 Python 源碼中最核心的部分,給出了詳細(xì)的闡釋,不過閱讀此書需要對 C 語言內(nèi)存模型和指針有著很好的理解。
另外,Python 本身是一門雜糅多種范式的動態(tài)語言,也就是說,相對于 C 的過程式、 Haskell 等的函數(shù)式、Java 基于類的面向?qū)ο蠖?,它都不夠純粹。換而言之,編程語言的“道學(xué)”,在 Python 中只能有限的體悟。學(xué)習(xí)某種編程范式時,從那些面向這種范式更加純粹的語言出發(fā),才能有更深刻的理解,也能了解到 Python 語言的根源。
這里推薦一門公開課
「編程范式」:斯坦福大學(xué)公開課:編程范式
講師高屋建瓴,從各種編程范式的代表語言出發(fā),給出了每種編程范式最核心的思想。
值得一提的是,這門課程對C語言有非常深入的講解,例如C語言的范型和內(nèi)存管理。這些知識,對閱讀 Python 源碼也有大有幫助。
Python 的許多最佳實(shí)踐都隱藏在那些眾所周知的框架和類庫中,例如 Django、Tornado 等等。在它們的源代碼中淘金,也是個不錯的選擇。
? ?最后的話
每個人學(xué)編程的道路都是不一樣的,其實(shí)大都殊途同歸,沒有迷路的人只有不能堅持的人!
希望想學(xué) Python 想學(xué)編程的同學(xué),不要猶豫了,看完這篇文章,
Just Getting Started ?!??!
你好,我理解你是希望python運(yùn)行程序的時候,將python原來的那個運(yùn)行環(huán)境最小化,下面是一個例子:
import?ctypes
ctypes.windll.user32.ShowWindow(?ctypes.windll.kernel32.GetConsoleWindow(),?6?)
運(yùn)行的話,那個python運(yùn)行的界面就會自動最小化了。
對大多數(shù)軟件開發(fā)者而言,術(shù)語數(shù)據(jù)庫通常是指RDBMS(關(guān)系數(shù)據(jù)庫管理系統(tǒng)), 這些系統(tǒng)使用表格(類似于電子表格的網(wǎng)格),其中行表示記錄,列表示記錄的字段。表格及其中存放的數(shù)據(jù)是使用SQL (結(jié)構(gòu)化査詢語言)編寫的語句來創(chuàng)建并操縱的。Python提供了用于操縱SQL數(shù)據(jù)庫的API(應(yīng)用程序接口),通常與作為標(biāo)準(zhǔn)的SQLite 3數(shù)據(jù)庫一起發(fā)布。
另一種數(shù)據(jù)庫是DBM (數(shù)據(jù)庫管理器),其中存放任意數(shù)量的鍵-值項(xiàng)。Python 的標(biāo)準(zhǔn)庫提供了幾種DBM的接口,包括某些特定于UNIX平臺的。DBM的工作方式 與Python中的字典類似,區(qū)別在于DBM通常存放于磁盤上而不是內(nèi)存中,并且其鍵與值總是bytes對象,并可能受到長度限制。本章第一節(jié)中講解的shelve模塊提供了方便的DBM接口,允許我們使用字符串作為鍵,使用任意(picklable)對象作為值。
如果可用的 DBM 與 SQLite 數(shù)據(jù)庫不夠充分,Python Package Index, pypi.python.org/pypi中提供了大量數(shù)據(jù)庫相關(guān)的包,包括bsddb DBM ("Berkeley DB"),對象-關(guān)系映射器,比如SQLAlchemy (),以及流行的客戶端/服務(wù)器數(shù)據(jù)的接口,比如 DB2、Informix、Ingres、MySQL、ODBC 以及 PostgreSQL。
本章中,我們將實(shí)現(xiàn)某程序的兩個版本,該程序用于維護(hù)一個DVD列表,并追蹤每個DVD的標(biāo)題、發(fā)行年份、時間長度以及發(fā)行者。該程序的第一版使用DBM (通過shelve模塊)存放其數(shù)據(jù),第二版則使用SQLite數(shù)據(jù)庫。兩個程序都可以加載與保存簡單的XML格式,這使得從某個程序?qū)С鯠VD數(shù)據(jù)并將其導(dǎo)入到其他程序成為可能。與DBM版相比,基于SQL的程序提供了更多一些的功能,并且其數(shù)據(jù)設(shè)計也稍干凈一些。
12.1 DBM數(shù)據(jù)庫
shelve模塊為DBM提供了一個wrapper,借助于此,我們在與DBM交互時,可以將其看做一個字典,這里是假定我們只使用字符串鍵與picklable值,實(shí)際處理時, shelve模塊會將鍵與值轉(zhuǎn)換為bytes對象(或者反過來)。
由于shelve模塊使用的是底層的DBM,因此,如果其他計算機(jī)上沒有同樣的DBM,那么在某臺計算機(jī)上保存的DBM文件在其他機(jī)器上無法讀取是可能的。為解決這一問題,常見的解決方案是對那些必須在機(jī)器之間可傳輸?shù)奈募峁ML導(dǎo)入與導(dǎo)出功能,這也是我們在本節(jié)的DVD程序dvds-dbm.py中所做的。
對鍵,我們使用DVD的標(biāo)題;對值,則使用元組,其中存放發(fā)行者、發(fā)行年份以及時間。借助于shelve模塊,我們不需要進(jìn)行任何數(shù)據(jù)轉(zhuǎn)換,并可以把DBM對象當(dāng)做一個字典進(jìn)行處理。
程序在結(jié)構(gòu)上類似于我們前面看到的那種菜單驅(qū)動型的程序,因此,這里主要展示的是與DBM程序設(shè)計相關(guān)的那部分。下面給出的是程序main()函數(shù)中的一部分, 忽略了其中菜單處理的部分代碼。
db = None
try:
db = shelve.open(filename, protocol=pickle.HIGHEST_PROTOCOL)
finally:
if db is not None:
db.dose()
這里我們已打開(如果不存在就創(chuàng)建)指定的DBM文件,以便于對其進(jìn)行讀寫操作。每一項(xiàng)的值使用指定的pickle協(xié)議保存為一個pickle,現(xiàn)有的項(xiàng)可以被讀取, 即便是使用更底層的協(xié)議保存的,因?yàn)镻ython可以計算出用于讀取pickle的正確協(xié)議。最后,DBM被關(guān)閉——其作用是清除DBM的內(nèi)部緩存,并確保磁盤文件可以反映出已作的任何改變,此外,文件也需要關(guān)閉。
該程序提供了用于添加、編輯、列出、移除、導(dǎo)入、導(dǎo)出DVD數(shù)據(jù)的相應(yīng)選項(xiàng)。除添加外,我們將忽略大部分用戶接口代碼,同樣是因?yàn)橐呀?jīng)在其他上下文中進(jìn)行了展示。
def add_dvd(db):
title = Console.get_string("Title", "title")
if not title:
return
director = Console.get_string("Director", "director")
if not director:
return
year = Console.get_integer("Year", "year",minimum=1896,
maximum=datetime,date.today().year)
duration = Console.get_integer("Duration (minutes)", "minutes“, minimum=0, maximum=60*48)
db[title] = (director, year, duration)
db.sync()
像程序菜單調(diào)用的所有函數(shù)一樣,這一函數(shù)也以DBM對象(db)作為其唯一參數(shù)。該函數(shù)的大部分工作都是獲取DVD的詳細(xì)資料,在倒數(shù)第二行,我們將鍵-值項(xiàng)存儲在DBM文件中,DVD的標(biāo)題作為鍵,發(fā)行者、年份以及時間(由shelve模塊pickled在一起)作為值。
為與Python通常的一致性同步,DBM提供了與字典一樣的API,因此,除了 shelve.open() 函數(shù)(前面已展示)與shelve.Shelf.sync()方法(該方法用于清除shelve的內(nèi)部緩存,并對磁盤上文件的數(shù)據(jù)與所做的改變進(jìn)行同步——這里就是添加一個新項(xiàng)),我們不需要學(xué)習(xí)任何新語法。
def edit_dvd(db):
old_title = find_dvd(db, "edit")
if old_title is None:
return
title = Console.get.string("Title", "title", old_title)
if not title:
return
director, year, duration = db[old_title]
...
db[title]= (director, year, duration)
if title != old_title:
del db[old_title]
db.sync()
為對某個DVD進(jìn)行編輯,用戶必須首先選擇要操作的DVD,也就是獲取DVD 的標(biāo)題,因?yàn)闃?biāo)題用作鍵,值則用于存放其他相關(guān)數(shù)據(jù)。由于必要的功能在其他場合 (比如移除DVD)也需要使用,因此我們將其實(shí)現(xiàn)在一個單獨(dú)的find_dvd()函數(shù)中,稍后將査看該函數(shù)。如果找到了該DVD,我們就獲取用戶所做的改變,并使用現(xiàn)有值作為默認(rèn)值,以便提高交互的速度。(對于這一函數(shù),我們忽略了大部分用戶接口代碼, 因?yàn)槠渑c添加DVD時幾乎是相同的。)最后,我們保存數(shù)據(jù),就像添加時所做的一樣。如果標(biāo)題未作改變,就重寫相關(guān)聯(lián)的值;如果標(biāo)題已改變,就創(chuàng)建一個新的鍵-值對, 并且需要刪除原始項(xiàng)。
def find_dvd(db, message):
message = "(Start of) title to " + message
while True:
matches =[]
start = Console.get_string(message, "title")
if not start:
return None
for title in db:
if title.lower().startswith(start.lower()):
matches.append(title)
if len(matches) == 0:
print("There are no dvds starting with", start)
continue
elif len(matches) == 1:
return matches[0]
elif len(matches) DISPLAY_LIMIT:
print("Too many dvds start with {0}; try entering more of the title".format(start)
continue
else:
matches = sorted(matches, key=str.lower)
for i, match in enumerate(matches):
print("{0}: {1}".format(i+1, match))
which = Console.get_integer("Number (or 0 to cancel)",
"number", minimum=1, maximum=len(matches))
return matches[which - 1] if which != 0 else None
為盡可能快而容易地發(fā)現(xiàn)某個DVD,我們需要用戶只輸入其標(biāo)題的一個或頭幾個字符。在具備了標(biāo)題的起始字符后,我們在DBM中迭代并創(chuàng)建一個匹配列表。如果只有一個匹配項(xiàng),就返回該項(xiàng);如果有幾個匹配項(xiàng)(但少于DISPLAY_LIMIT, 一個在程序中其他地方設(shè)置的整數(shù)),就以大小寫不敏感的順序展示所有這些匹配項(xiàng),并為每一項(xiàng)設(shè)置一個編號,以便用戶可以只輸入編號就可以選擇某個標(biāo)題。(Console.get_integer()函數(shù)可以接受0,即便最小值大于0,以便0可以用作一個刪除值。通過使用參數(shù)allow_zero=False, 可以禁止這種行為。我們不能使用Enter鍵,也就是說,沒有什么意味著取消,因?yàn)槭裁匆膊惠斎胍馕吨邮苣J(rèn)值。)
def list_dvds(db):
start =”"
if len(db) DISPLAY.LIMIT:
start = Console.get_string(“List those starting with [Enter=all]”, "start”)
print()
for title in sorted(db, key=str.lower):
if not start or title.Iower().startswith(start.lower()):
director, year, duration = db[title]
print("{title} ({year}) {duration} minute{0}, by "
"{director}".format(Util.s(duration),**locals()))
列出所有DVD (或者那些標(biāo)題以某個子字符串引導(dǎo))就是對DBM的所有項(xiàng)進(jìn)行迭代。
Util.s()函數(shù)就是簡單的s = lambda x: "" if x == 1 else "s",因此,如果時間長度不是1分鐘,就返回"s"。
def remove_dvd(db):
title = find_dvd(db, "remove")
if title is None:
return
ans = Console.get_bool("Remove {0}?".format(title), "no")
if ans:
del db[title]
db.sync()
要移除一個DVD,首先需要找到用戶要移除的DVD,并請求確認(rèn),獲取后從DBM中刪除該項(xiàng)即可。
到這里,我們展示了如何使用shelve模塊打開(或創(chuàng)建)一個DBM文件,以及如何向其中添加項(xiàng)、編輯項(xiàng)、對其項(xiàng)進(jìn)行迭代以及移除某個項(xiàng)。
遺憾的是,在我們的數(shù)據(jù)設(shè)計中存在一個瑕疵。發(fā)行者名稱是重復(fù)的,這很容易導(dǎo)致不一致性,比如,發(fā)行者Danny DeVito可能被輸入為"Danny De Vito",用于 一個電影;也可以輸入為“Danny deVito",用于另一個。為解決這一問題,可以使用兩個DBM文件,主DVD文件使用標(biāo)題鍵與(年份,時間長度,發(fā)行者ID)值; 發(fā)行者文件使用發(fā)行者ID (整數(shù))鍵與發(fā)行者名稱值。下一節(jié)展示的SQL數(shù)據(jù)庫 版程序?qū)⒈苊膺@一瑕疵,這是通過使用兩個表格實(shí)現(xiàn)的,一個用于DVD,另一個用于發(fā)行者。
12.2 SQL數(shù)據(jù)庫
大多數(shù)流行的SQL數(shù)據(jù)庫的接口在第三方模塊中是可用的,Python帶有sqlite3 模塊(以及SQLite 3數(shù)據(jù)庫),因此,在Python中,可以直接開始數(shù)據(jù)庫程序設(shè)計。SQLite是一個輕量級的SQL數(shù)據(jù)庫,缺少很多諸如PostgreSQL這種數(shù)據(jù)庫的功能, 但非常便于構(gòu)造原型系統(tǒng),并且在很多情況下也是夠用的。
為使后臺數(shù)據(jù)庫之間的切換盡可能容易,PEP 249 (Python Database API Specification v2.0)提供了稱為DB-API 2.0的API規(guī)范。數(shù)據(jù)庫接口應(yīng)該遵循這一規(guī)范,比如sqlite3模塊就遵循這一規(guī)范,但不是所有第三方模塊都遵循。API規(guī)范中指定了兩種主要的對象,即連接對象與游標(biāo)對象。表12-1與表12-2中分別列出了這兩種對象必須支持的API。在sqlite3模塊中,除DB-API 2.0規(guī)范必需的之外,其連接對象與游標(biāo)對象都提供了很多附加的屬性與方法。
DVD程序的SQL版本為dvds.sql.py,該程序?qū)l(fā)行者與DVD數(shù)據(jù)分開存儲,以 避免重復(fù),并提供一個新菜單,以供用戶列出發(fā)行者。該程序使用的兩個表格在圖12-1
def connect(filename):
create= not os.path.exists(filename)
db = sqlite3.connect(filename)
if create:
cursor = db.cursor()
cursor.execute("CREATE TABLE directors ("
"id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, "
"name TEXT UNIQUE NOT NULL)")
cursor.execute("CREATE TABLE dvds ("
"id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, "
"title TEXT NOT NULL, "
"year INTEGER NOT NULL,"
"duration INTEGER NOT NULL, "
"director_id INTEGER NOT NULL, ”
"FOREIGN KEY (director_id) REFERENCES directors)")
db.commit()
return db
sqlite3.connect()函數(shù)會返回一個數(shù)據(jù)庫對象,并打開其指定的數(shù)據(jù)庫文件。如果該文件不存在,就創(chuàng)建一個空的數(shù)據(jù)庫文件。鑒于此,在調(diào)用sqlite3.connect()之前,我們要注意數(shù)據(jù)庫是否是準(zhǔn)備從頭開始創(chuàng)建,如果是,就必須創(chuàng)建該程序要使用的表格。所有査詢都是通過一個數(shù)據(jù)庫游標(biāo)完成的,可以從數(shù)據(jù)庫對象的cursor()方法獲取。
注意,兩個表格都是使用一個ID字段創(chuàng)建的,ID字段有一個AUTOINCREMENT 約束——這意味著SQLite會自動為ID字段賦予唯一性的數(shù)值,因此,在插入新記錄時,我們可以將這些字段留給SQLite處理。
SQLite支持有限的數(shù)據(jù)類型——實(shí)際上就是布爾型、數(shù)值型與字符串——但使用數(shù)據(jù)'‘適配器”可以對其進(jìn)行擴(kuò)展,或者是擴(kuò)展到預(yù)定義的數(shù)據(jù)類型(比如那些用于日期與datetimes的類型),或者是用于表示任意數(shù)據(jù)類型的自定義類型。DVD程序并不需要這一功能,如果需要,sqlite3模塊的文檔提供了很多詳細(xì)解釋。我們使用的外部鍵語法可能與用于其他數(shù)據(jù)庫的語法不同,并且在任何情況下,只是記錄我們的意圖,因?yàn)镾QLite不像很多其他數(shù)據(jù)庫那樣需要強(qiáng)制關(guān)系完整性,sqlite3另一點(diǎn)與眾不同的地方在于其默認(rèn)行為是支持隱式的事務(wù)處理,因此,沒有提供顯式的“開始事務(wù)” 方法。
def add_dvd(db):
title = Console.get_string("Title", "title")
if not title:
return
director = Console.get_string("Director", "director")
if not director:
return
year = Console.get_integer("Year", "year”, minimum=1896,
maximum=datetime.date.today().year)
duration = Console.get_integer("Duration (minutes)", "minutes",
minimum=0,maximum=60*48)
director_id = get_and_set_director(db, director)
cursor = db.cursor()
cursor.execute("INSERT INTO dvds ”
"(title, year, duration, director_id)"
"VALUES (?, ?, ?, ?)",
(title, year, duration, director_id))
db.commit()
這一函數(shù)的開始代碼與dvds-dbm.py程序中的對應(yīng)函數(shù)一樣,但在完成數(shù)據(jù)的收集后,與原來的函數(shù)有很大的差別。用戶輸入的發(fā)行者可能在也可能不在directors表格中,因此,我們有一個get_and_set_director()函數(shù),在數(shù)據(jù)庫中尚無某個發(fā)行者時, 該函數(shù)就將其插入到其中,無論哪種情況都返回就緒的發(fā)行者ID,以便在需要的時候插入到dvds表。在所有數(shù)據(jù)都可用后,我們執(zhí)行一條SQL INSERT語句。我們不需要指定記錄ID,因?yàn)镾QLite會自動為我們提供。
在査詢中,我們使用問號(?)作為占位符,每個?都由包含SQL語句的字符串后面的序列中的值替代。命名的占位符也可以使用,后面在編輯記錄時我們將看到。盡管避免使用占位符(而只是簡單地使用嵌入到其中的數(shù)據(jù)來格式化SQL字符串)也是可能的,我們建議總是使用占位符,并將數(shù)據(jù)項(xiàng)正確編碼與轉(zhuǎn)義的工作留給數(shù)據(jù)庫模塊來完成。使用占位符的另一個好處是可以提高安全性,因?yàn)檫@可以防止任意的SQL 被惡意地插入到一個査詢中。
def get_and_set_director(db, director):
director_id = get_director_id(db, director)
if directorjd is not None:
return director_id
cursor = db.cursor()
cursor.execute("lNSERT INTO directors (name) VALUES (?)”,(director,))
db.commit()
return get_director_id(db, director)
這一函數(shù)返回給定發(fā)行者的ID,并在必要的時候插入新的發(fā)行者記錄。如果某個記錄被插入,我們首先嘗試使用get_director_id()函數(shù)取回其ID。
def get_director_id(db, director):
cursor = db.cursor()
cursor.execute("SELECT id FROM directors WHERE name=?",(director,))
fields = cursor.fetchone()
return fields[0] if fields is not None else None
get_director_id()函數(shù)返回給定發(fā)行者的ID,如果數(shù)據(jù)庫中沒有指定的發(fā)行者,就返回None。我們使用fetchone()方法,因?yàn)榛蛘哂幸粋€匹配的記錄,或者沒有。(我們知道,不會有重復(fù)的發(fā)行者,因?yàn)閐irectors表格的名稱字段有一個UNIQUE約束,在任何情況下,在添加一個新的發(fā)行者之前,我們總是先檢査其是否存在。)這種取回方法總是返回一個字段序列(如果沒有更多的記錄,就返回None)。即便如此,這里我們只是請求返回一個單獨(dú)的字段。
def edit_dvd(db):
title, identity = find_dvd(db, "edit")
if title is None:
return
title = Console.get_string("Title","title", title)
if not title:
return
cursor = db.cursor()
cursor.execute("SELECT dvds.year, dvds.duration, directors.name"
“FROM dvds, directors "
"WHERE dvds.director_id = directors.id AND "
"dvds.id=:id", dict(id=identity))
year, duration, director = cursor.fetchone()
director = Console.get_string("Director", "director", director)
if not director:
return
year = Console,get_integer("Year","year", year, 1896,datetime.date.today().year)
duration = Console.get_integer("Duration (minutes)", "minutes",
duration, minimum=0, maximum=60*48)
director_id = get_and_set_director(db, director)
cursor.execute("UPDATE dvds SET title=:title, year=:year,"
"duration=:duration, director_id=:directorjd "
"WHERE id=:identity", locals())
db.commit()
要編輯DVD記錄,我們必須首先找到用戶需要操縱的記錄。如果找到了某個記錄,我們就給用戶修改其標(biāo)題的機(jī)會,之后取回該記錄的其他字段,以便將現(xiàn)有值作為默認(rèn)值,將用戶的輸入工作最小化,用戶只需要按Enter鍵就可以接受默認(rèn)值。這里,我們使用了命名的占位符(形式為:name),并且必須使用映射來提供相應(yīng)的值。對SELECT語句,我們使用一個新創(chuàng)建的字典;對UPDATE語句,我們使用的是由 locals()返回的字典。
我們可以同時為這兩個語句都使用新字典,這種情況下,對UPDATE語句,我們可以傳遞 dict(title=title, year=year, duration=duration, director_id=director_id, id=identity)),而非 locals()。
在具備所有字段并且用戶已經(jīng)輸入了需要做的改變之后,我們?nèi)』叵鄳?yīng)的發(fā)行者ID (如果必要就插入新的發(fā)行者記錄),之后使用新數(shù)據(jù)對數(shù)據(jù)庫進(jìn)行更新。我們采用了一種簡化的方法,對記錄的所有字段進(jìn)行更新,而不僅僅是那些做了修改的字段。
在使用DBM文件時,DVD標(biāo)題被用作鍵,因此,如果標(biāo)題進(jìn)行了修改,我們就需要創(chuàng)建一個新的鍵-值項(xiàng),并刪除原始項(xiàng)。不過,這里每個DVD記錄都有一個唯一性的ID,該ID是記錄初次插入時創(chuàng)建的,因此,我們只需要改變?nèi)魏纹渌侄蔚闹担?而不需要其他操作。
def find_dvd(db, message):
message = "(Start of) title to " + message
cursor = db.cursor()
while True: .
start = Console.get_stnng(message, "title")
if not start:
return (None, None)
cursor.execute("SELECT title, id FROM dvds "
"WHERE title LIKE ? ORDER BY title”,
(start +"%",))
records = cursor.fetchall()
if len(records) == 0:
print("There are no dvds starting with", start)
continue
elif len(records) == 1: