iT邦幫忙

2023 iThome 鐵人賽

DAY 30
1

前言

前面我們實作出了一些基本的模型,而今天我們就拿之前的多元線性回歸模型來當作範例,要把這個模型佈署至網頁上,藉由讓使用者從輸入框傳入特徵資料到模型中,讓模型能夠把輸出結果顯示在網頁上,而特徵資料會包含使用者的年資、教育程度和居住城市,模型就會依據這些資料做年薪的預測。

輸入特徵

對於特徵資料的取得,我們會讓使用者用 <form> 表單的方式,用輸入框和下拉式選單去做資料傳入,所以就到 templates/index.html 中去設計我們的表單架構,注意到 標籤我們會通過指定 for 屬性的方式,讓我們能夠在 view.py 中獲取使用者輸入的資料,下面為 index.html 內容:

<html>

<body>
    <div align="center">
        <h1>Multiple Linear Regression</h1>
        <form method="post">
            {% csrf_token %}
            <input type="text" name="seniority" placeholder="輸入年資">
    
            <label for="Education">教育程度:</label>
            <select name="Education" id="city">
                <option value="高中以下">高中以下</option>
                <option value="大學">大學</option>
                <option value="碩士以上">碩士以上</option>
            </select>
    
            <label for="City">城市:</label>
            <select name="City" id="city">
                <option value="CityA">CityA</option>
                <option value="CityB">CityB</option>
                <option value="CityC">CityC</option>
            </select>
            <button type="submit">提交</button>
        </form>
    
        {% if result %}
        <h2>用戶資料:</h2>
        <table border="1" width="500">
            <tr align="center">
                <th>年資</th>
                <th>教育程度</th>
                <th>城市</th>
                <th>預測年薪</th>
            </tr>
            <tr align="center">
                <td>{{ result.seniority }}</td>
                <td>{{ result.education }}</td>
                <td>{{ result.city }}</td>
                <td>{{ result.salary }}</td>
            </tr>
        </table>
        {% endif %}
    </div>

</body>

</html>

模型搭建

若我們在 jupyter lab ( 或其他 IDE ) 上訓練出我們的多元線性回歸模型,那要怎麼把它與 Django 結合呢?下面是多元線性回歸模型的完整訓練程式碼,我們會讓 Django 的應用程式去使用這個模型:

使用套件

import torch
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np
from torch import nn

資料預處理

這邊用 One-Hot Encoding 與 Label Encoding 去處理包含字串資料的欄位,最後得到四個特徵欄位與一個標籤欄位,再去訓練與測試資料的切割,讓測試資料的數量占全部的 20 %,最後用 StandardScaler() 做特徵資料的標準化:

np.set_printoptions(formatter={"float": "{: .2e}".format})
data = pd.read_csv("Salary_Data2.csv")
data["EducationLevel"] = data["EducationLevel"].map({"高中以下": 0, "大學": 1, "碩士以上": 2})
oneHot_encoder = OneHotEncoder()
oneHot_encoder.fit(data[["City"]])
city_encoded = oneHot_encoder.transform(data[["City"]]).toarray()
data[["CityA", "CityB", "CityC"]] = city_encoded

data = data.drop(["City", "CityC"], axis=1)
x = data[["YearsExperience", "EducationLevel", "CityA", "CityB"]]  # Feature
y = data["Salary"]  # Label
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=87)
x_train = x_train.to_numpy()
x_test = x_test.to_numpy()
y_train = y_train.to_numpy()
y_test = y_test.to_numpy()

scaler = StandardScaler()
scaler.fit(x_train)
x_train = scaler.transform(x_train)
x_test = scaler.transform(x_test)

x_train = torch.from_numpy(x_train)
y_train = torch.from_numpy(y_train)
x_test = torch.from_numpy(x_test)
y_test = torch.from_numpy(y_test)

模型設計

定義我們的多元線性回歸模型,用 nn.Linear() 增加了一個線性層,相當於神經網路中設定輸入層神經元數量 ( in_features ) 有 4 個,最後輸出層數量 ( out_features ) 為 1 個值:

class LinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear_layer = nn.Linear(in_features=4, out_features=1, dtype=torch.float64)

    def forward(self, x):
        return self.linear_layer(x)

建立模型物件

這邊宣告多元線性回歸模型物件 model,宣告後模型就會得到一組初始參數,並且用 reshape 去調整資料的形狀,以便之後訓練時能夠進行與參數之間的運算 ( 矩陣的相乘運算 ) :

torch.manual_seed(87)
model = LinearRegressionModel()

x_train = x_train.reshape(-1, 4)  
y_train = y_train.reshape(-1, 1)
x_test = x_test.reshape(-1, 4)
y_test = y_test.reshape(-1, 1)

損失函數與優化器

這邊設定損失函數為 MSE ( 均方誤差 ),優化器則使用 SGD ( 隨機梯度下降法 ),並設定 lr學習率為 0.001,params 參數的地方則是傳入模型的初始參數:

mseLoss = nn.MSELoss()  # mean square error
optimizer = torch.optim.SGD(params=model.parameters(), lr=0.001)

訓練模型

之前有跟著實作過模型的話這段一定不陌生吧,我們讓模型去訓練迭代一萬次,並把模型在每次迭代中,對於訓練資料 ( x_train , y_train ) 和驗證資料 ( x_test , y_test ) 上的表現 ( 成本損失值 ) 印出來:

train_cost_hist = []
test_cost_hist = []
for epoch in range(10000):

    model.train()  # Training Mode On
    train_pred = model(x_train)
    train_cost = mseLoss(train_pred, y_train)
    train_cost_hist.append(train_cost.detach().numpy())
    optimizer.zero_grad()
    train_cost.backward()  # 計算目前梯度並更新 model.w.grad
    optimizer.step()  # 更新參數

    model.eval()  # Evaluation Mode On
    with torch.inference_mode():  # Inference Mode On
        test_pred = model(x_test)
        test_cost = mseLoss(test_pred, y_test)
        test_cost_hist.append(test_cost.detach().numpy())
    if (epoch % 1000 == 0):
        print(
            f"epoch:{epoch: 5}, train_cost:{train_cost: .4e}, testing_cost:{test_cost: .4e}")

儲存模型

我們可以嘗試將模型最終訓練出來的參數印出來,結果會出現 OrderedDict 類型的物件:

model.state_dict()

輸出結果:

OrderedDict([('linear_layer.weight',
tensor([[ 4.0801, 14.1181, -1.4940, -3.7587]], dtype=torch.float64)),
('linear_layer.bias', tensor([50.9500], dtype=torch.float64))])

而要讓我們 Django 的應用程式可以使用到這個模型,這步驟就很重要,我們用下面程式碼去儲存我們訓練好的多元線性回歸模型,也就是儲存模型訓練出來的最終參數,注意到 f 參數需要指定為你想要把模型儲存的路徑位置,所以我們把模型存放在 model 目錄中,並且檔案附檔名為.pth 檔,表示可以用來儲存我們訓練好的模型,所以說這檔案就代表了模型的參數:

torch.save(obj=model.state_dict(), f="model/MLR_Model.pth")

Django 部分

在儲存好模型之後,我們就回到 Django 應用程式的部分,在應用程式目錄中創建一個資料夾 models,並且在裡面存放剛才的模型 .pth 檔,這個就可以在應用程式中去使用這個模型了:

https://ithelp.ithome.com.tw/upload/images/20231015/20158157k19msA7kHA.png

https://drive.google.com/file/d/1ZkqE1IpTieSTrnc9kUsRukK5uoRBi91m/view?usp=drive_link

然後我們在應用程式目錄下再新增一個目錄 datasets,用來存放我們的資料集檔案 Salary_Data2.csv ( 檔案可在上方連結取得 ):

https://ithelp.ithome.com.tw/upload/images/20231015/20158157WLIWlawDcw.png

下面是 views.py 中的完整程式碼:

from django.shortcuts import render
from django.http import HttpResponse
import numpy as np
import pandas as pd
import torch
from torch import nn
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.model_selection import train_test_split

# Create your views here.

class LinearRegressionModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear_layer = nn.Linear(in_features=4, out_features=1, dtype=torch.float64)

    def forward(self, x):
        return self.linear_layer(x)

# 模型路徑
model_path = 'C:\\Users\Thomas\Desktop\ithome\MLR_Model\models\MLR_Model.pth'

# 下面這段是為了得到 scaler 在 training data 上的參數 (mean, std), testing data 才得以沿用

data = pd.read_csv("C:\\Users\Thomas\Desktop\ithome\MLR_Model\datasets\Salary_Data2.csv")

# Label Encoding 處理
data["EducationLevel"] = data["EducationLevel"].map({"高中以下": 0, "大學": 1, "碩士以上": 2})

# One-Hot Encoding 獨熱編碼處理字串格式資料
oneHot_encoder = OneHotEncoder()
oneHot_encoder.fit(data[["City"]])
city_encoded = oneHot_encoder.transform(data[["City"]]).toarray()
data[["CityA", "CityB", "CityC"]] = city_encoded
data = data.drop(["City", "CityC"], axis=1)

# 劃分特徵與標籤資料
x = data[["YearsExperience", "EducationLevel", "CityA", "CityB"]]  # Feature
y = data["Salary"]  # Label

# 資料切割
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=87)
x_train = x_train.to_numpy()
x_test = x_test.to_numpy()

# 特徵資料標準化
scaler = StandardScaler()
scaler.fit(x_train)
x_train = scaler.transform(x_train)
x_test = scaler.transform(x_test)

# 對使用者輸入的資料做資料預處理
def data_preprocessing(data):
    df = pd.DataFrame([data], columns=["Seniority", "Education", "City"])
    flag = np.array([['CityA'], ['CityB'], ['CityC']])
    x = np.where(flag == data[2])[0][0]
    df["Education"] = df["Education"].map({"高中以下": 0, "大學": 1, "碩士以上": 2})
    oneHot_encoder = OneHotEncoder()
    oneHot_encoder.fit(flag)
    city_encoded = oneHot_encoder.transform(flag).toarray()
    df[["CityA", "CityB", "CityC"]] = city_encoded[x]
    df = df.drop(["City", "CityC"], axis=1)
    feature = [df.iloc[0].tolist()]
    feature = np.array(feature)
    feature = scaler.transform(feature)

    return feature

# 對於使用者輸入的資料去做預測
def model(feature):

		# 使用預先訓練好的模型參數
    model = LinearRegressionModel()
    model.load_state_dict(torch.load(f=model_path)) # 載入模型

		# 取得模型參數
    w_final = model.state_dict()['linear_layer.weight'][0].tolist()
    b_final = model.state_dict()['linear_layer.bias'][0].item()
		
		# 進行模型預測
    y_pred = (feature * w_final).sum(axis=1) + b_final

    return y_pred

def say_hello(request):
    if request.method == 'POST':
        # 取得使用者的輸入特徵資料
        user_seniority = float(request.POST.get('seniority', ''))
        user_education = request.POST.get('Education', '')
        user_city = request.POST.get('City', '')

        # 進行特徵資料處理
        user_data = [user_seniority, user_education, user_city]
        feature = data_preprocessing(user_data)

        # 模型預測
        user_salary = str(round(model(feature).item(), 1)) + '(萬)'

        result_list = {
            'seniority': user_seniority,
            'education': user_education,
            'city': user_city,
            'salary': user_salary
        }

        return render(request, 'index.html', {'result': result_list})
    return render(request, 'index.html')

可以用 pipenv install [套件名稱] 去安裝所需的套件,像是下面我們想安裝 numpy 套件:

https://ithelp.ithome.com.tw/upload/images/20231015/20158157xhpLLtUAM7.png

在這個虛擬環境中,可以找到 Pipfile 檔案,裡面就會顯示當前環境中的一些資訊,包括套件或是版本資訊等,像是我這邊就已經安裝好了 django、numpy、pandas、torch、scikit-learn 套件,就可以在 [packages] 下面看到我們所在這個虛擬環境安裝的套件有哪些:

https://ithelp.ithome.com.tw/upload/images/20231015/201581573fY4ddZiDb.png

這邊來說明一下 say_hello 方法的部分,request.POST.get(’seniority’, ‘’) 表示要獲取在 index.html 檔中 標籤 for 屬性為 ‘seniority’ 的內容,這方法能夠讓我們在 view.py 獲取到使用者的輸入表單資料,我們使用者輸入的資料有三個,對應到 標籤的 for 屬性分別就是 ‘seniority’、’Education’、’City’,取得這些標籤裡的內容值,再 views.py 中經過資料處理後,會把它存放在 result_list 的字典中,然後指派到 index.html 檔案中的 result 變數,再透過 table 的方式把分別把字典中的值顯示出來:

def say_hello(request):
    if request.method == 'POST':

        # 取得使用者的輸入特徵資料
        user_seniority = float(request.POST.get('seniority', ''))
        user_education = request.POST.get('Education', '')
        user_city = request.POST.get('City', '')

        # 進行特徵資料處理
        user_data = [user_seniority, user_education, user_city]
        feature = data_preprocessing(user_data)

        # 模型預測
        user_salary = str(round(model(feature).item(), 1)) + '(萬)'

        result_list = {
            'seniority': user_seniority,
            'education': user_education,
            'city': user_city,
            'salary': user_salary
        }

        return render(request, 'index.html', {'result': result_list})
    return render(request, 'index.html')

下面顯示的 index.html 檔跟 view.py 檔案之間資料的傳遞關係,{% if result %} 判斷如果使用者三個特徵資料都有輸入的話並按下提交按鈕的話,就會用 table 去顯示輸入資料與預測的結果,反之會報錯,記得最後都要加上 {% endif %} 結束判斷條件:

https://ithelp.ithome.com.tw/upload/images/20231015/20158157s3R344iRQL.png

結果展示

確認終端機無顯示報錯 ( 無報錯畫面如下 ):

https://ithelp.ithome.com.tw/upload/images/20231015/20158157ln2May6GeD.png

輸入 app 網址 就能夠看到下面的畫面,現在使用者都還沒輸入表單資料,所以 table 並不會顯示出來,只會顯示表單中的輸入框和下拉式選單:

https://ithelp.ithome.com.tw/upload/images/20231015/20158157avLX95bwou.png

我們這邊在輸入框輸入年資,並且用下拉式選單選擇教育程度和城市:

https://ithelp.ithome.com.tw/upload/images/20231015/20158157OThipbzh9F.png

按下了提交按鈕後,就可以看到你剛傳入的特徵資料和模型預測出來的年薪結果啦 ~

https://ithelp.ithome.com.tw/upload/images/20231015/20158157hXnKTsI4GF.png

完賽感言

哇哇哇完賽感言終於來了,總算可以輕鬆沒壓力的寫了,很慶幸我能夠寫到完賽感言,代表我真的持續了 30 天連續不間斷發文,但這過程對我來說,絕對不只有 30 天而已。

現在五專四年級的我從這個學期開始就要準備畢業專題,隨著人工智慧的浪潮日益高漲,我選擇其中的機器學習做為我專題研究的範疇,也因此開啟我與機器學習的邂逅,很感謝室友在暑假前就告訴我有這個機會讓我參與這個比賽,並且找到夥伴們一起組隊報名 : )

機器學習對我來說是 0 基礎,從來沒有接觸過的東西,所以在暑假時,我就開始看台大李弘毅教授的機器學習公開課程,多虧他的課才讓我深層了解機器學習本身就是由數學原理所構成,讓我深刻體會到,去了解機器學習背後的數學架構對於這領域的掌握是最有幫助的,所以我很感謝這個比賽讓我在學習的當中,有個地方可以去紀錄我學習的腳步,也能夠把重要的知識點寫成文章和大家分享。

正因為這是我從未做過的事情,所以我會開始去思考文章的架構要怎麼去編排,或是這段文字要怎麼去描述才會最清楚,開始會站在閱讀者的角度去撰寫文章,起初就是看著公開課的影片邊做筆記,或是想到甚麼就寫甚麼,到最後發現整個知識點都是零散的,內容會跳來跳去沒辦法前後連貫,當時發現這個問題我的文章已經寫了 16 篇了,也很懊惱當初怎麼沒早點意識到這個問題,加上筆記的內容絕大多都是教授口中講的話,並不是用我自己的方式去描述,所以我當時毅然捨棄先前的 16 文章,重新用自己的話去寫,嘗試把文章的性質做分類,把一些廢話太多的地方做修改,這樣讀起來就順暢多了。

也是因為有這個比賽才讓我學會如何去做時間規劃,可以說這樣的生活因此多了點忙碌和充實感?晚上 10 點多上完微積分回到宿舍後看到 Notion 文章空白的無力感,還有每天都差點忘記要發文的緊張感 ( 阿不就還好有隊友提醒 ),當然還有完賽時內心的成就感,就算最後結果只有完賽獎,參加鐵人賽的這個決定我也不會後悔,準備鐵人賽的這些日子將來我也不會忘記,帶給我的更多是能力上的蛻變,30 天的機器學習之旅就到此結束啦,我們下趟旅程見 ~


上一篇
【Day 29】Web 應用程式佈署 ( 一 )
系列文
戀 AI ing - 我與機器學習的邂逅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言