前端智慧化:人機協同的程式設計方式

設計稿智慧生成程式碼發表於2020-04-07

文/甄子

在 imgcook.com 釋出之後,我們在掘金、知乎、微信朋友圈和公眾號發表了很多文章,收穫最多的反饋是:前端要失業了。按照現在的趨勢來看,對 P5 左右前端工程師的需求將大幅度降低。imgcook.com 官網底部有統計,從資料分析智慧生成UI和邏輯程式碼的情況,imgcook 已經可以替代 P5 前端工程師完成工作的 80%。無論趨勢還是資料,都能看到傳統前端低技術價值、高重複的工作正走向消亡。

20200407001105.jpg

如馬雲先生在香港對年輕人傳授創業經驗時講到的,蒸汽機和電力解放了人類的體力,人工智慧和機器學習解放了人類的腦力。馬雲先生在評價蒸汽機和電力帶來的失業問題時講到,人類在科技進步下從繁重的體力勞動中解放出來,逐步向腦力勞動過渡,這是人類社會的進步。今天前端從重複勞動的施工隊裡解放出來,逐步向智慧化的高技術含量工作過渡,這是前端技術的進步。

綜上所述,今天已經無法繞過智慧化命題了,就像當今社會回不去刀耕火種的年代。預見到未來如此來臨,我們推出了 github.com/alibaba/pipcook 專案,試圖幫助前端零成本進入智慧化,聯合Google的Tensorflow.js團隊,用這個框架輕鬆打通智慧化的任督二脈:連線前端技術生態、複用Python技術生態。

因此,後文將在Pipcook框架的基礎上,從概念到方法、從方法到實踐、從工具到應用的方式,和大家分享:前端的那些工作會被智慧化取代?如何在工作中應用這種智慧化方法?前端工程師如何藉助 pipcook 和 AI 協同工作?最後,用imgcook詳細設計實現作為案例,對實踐進行系統的講解。

除了去 imgcook.com 體驗,這裡帶來一個 tensorflow.js 官方的示例,來看看智慧化離我們的距離。首先是定義問題:很多人都很困惑發動機馬力和行駛里程的關係是怎樣的?是否可以根據發動機馬力預測未來的這個車是否省油呢?傳統統計分析的方法是定義模式,用模型計算分析,人和計算機是下指令和執行的主僕關係。機器學習的方法是通過資料學習模式,用模型學到的模式進行分析,人和計算機是提供資料和理解資料的協同關係。下面用程式碼展示這種協同怎麼在Javascript裡實現:

環境準備:

index.html

<!DOCTYPE html>
<html>
<head>
  <title>TensorFlow.js Tutorial</title>

  <!-- Import TensorFlow.js -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
  <!-- Import tfjs-vis -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>

  <!-- Import the main script file -->
  <script src="script.js"></script>

</head>

<body>
</body>
</html>
複製程式碼

這裡Tensorflow.js就是谷歌官方的前端機器學習基礎庫,tfjs-vis是視覺化工具用於顯示資料、模型等資訊,script.js就是示例的程式碼。

script.js

示例的程式碼主要由四個部分構成:提供資料、定義模型、訓練模型、模型預測,此外就是執行邏輯的 run 函式。下面針對這四個部分詳細介紹一下。

提供資料:

提供資料分為:獲取資料、分析資料、處理資料三個階段。獲取資料可以理解為從現有的日誌、雲物件儲存等資料來源獲取原始資料。分析資料則是對資料進行觀察,尋找資料的問題:平均麼?平凡麼?符合現實場景麼?對於我們定義的問題來說描述充分麼?處理資料是根據分析的結果對資料進行校準,然後針對模型的訓練要求進行格式化。

獲取資料:

async function getData() {
  const carsDataReq = await fetch(
    "https://storage.googleapis.com/tfjs-tutorials/carsData.json"
  );
  const carsData = await carsDataReq.json();
  return carsData;
}
複製程式碼

分析資料:

async function run() {
  // Load and plot the original input data that we are going to train on.
  const data = await getData();
  const values = data.map((d) => ({
    x: d.horsepower,
    y: d.mpg,
  }));

  tfvis.render.scatterplot(
    { name: "Horsepower v MPG" },
    { values },
    {
      xLabel: "Horsepower",
      yLabel: "MPG",
      height: 300,
    }
  );
}

document.addEventListener("DOMContentLoaded", run);
複製程式碼

20200407001124.jpg

處理資料:

洗菜:先對原始資料做一些清洗:

function cleaned(data){
  const cleaned = carsData
      .map((car) => ({
        mpg: car.Miles_per_Gallon,
        horsepower: car.Horsepower,
      }))
      .filter((car) => car.mpg != null && car.horsepower != null);

  return cleaned;
}
複製程式碼

炒菜:再把資料加工成模型可以吃的樣子:

function convertToTensor(data) {
  // Wrapping these calculations in a tidy will dispose any
  // intermediate tensors.

  return tf.tidy(() => {
    // Step 1. Shuffle the data
    tf.util.shuffle(data);

    // Step 2. Convert data to Tensor
    const inputs = data.map((d) => d.horsepower);
    const labels = data.map((d) => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    //Step 3. Normalize the data to the range 0 - 1 using min-max scaling
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor
      .sub(inputMin)
      .div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor
      .sub(labelMin)
      .div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // Return the min/max bounds so we can use them later.
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    };
  });
}
複製程式碼

對於Tensor(張量)這個詞不用在意,只要理解為像 Json 一樣的資料組織格式就好了。這裡唯一需要理解的就是“歸一化”,因為資料可能有各種取值範圍,如何把資料統一到一個取值範圍內?上述程式碼把資料在 0 - 1 的範圍內,藉助極大值和極小值的約束對資料縮放,保證資料的本質並沒發生改變只是表達方式變了。

另一個概念就是Label(標籤),這個很好理解,每種馬力就是一個標籤,資料是每加侖行駛的里程,示例載入的資料就是一個個每加侖行駛里程的資料和這條資料對應的發動機馬力標籤。

...
{
  "mpg":15, //資料:每加侖行駛的里程
  "horsepower":165, //標籤:發動機馬力
},
{
  "mpg":18, //資料:每加侖行駛的里程
  "horsepower":150, //標籤:發動機馬力
},
{
  "mpg":16, //資料:每加侖行駛的里程
  "horsepower":150, //標籤:發動機馬力
},
...
複製程式碼

用這樣標註好的樣本資料:菜,洗菜、炒菜之後餵給模型,模型吃了就能消化理解資料背後的模式和規律。

定義模型

其實,除了研究機構和資深的學者外,真正能定義模型的人很少,演算法工程師經歷調參工程師、資料工程師後,仍然離定義模型很遠,大部分時間連演算法工程師都只是在應用而已。因此,去用就好了,定義模型只需要按照行業最流行、效果最好的模型抄作業即可。

/**
 * Define Model
 */
function createModel() {
  // Create a sequential model
  const model = tf.sequential();

  // Add a single input layer
  model.add(tf.layers.dense({ inputShape: [1], units: 1, useBias: true }));
  model.add(tf.layers.dense({ units: 50, activation: "sigmoid" }));
  // Add an output layer
  model.add(tf.layers.dense({ units: 1, useBias: true }));

  return model;
}
複製程式碼

如果你使用 github.com/alibaba/pipcook 連抄都省了,因為常用的模型我們和社群生態會提前抄好(移植好)。如果你需要抄一個,很簡單,這裡 tf 就是 tensorflow 的官方庫名,函式定義和引數定義和 Python 裡是一致的(語言特性造成的書寫差異還需要具體修改),只要把 Tensorflow 官網提供的模型裡的定義抄過來就好了。

20200407001140.jpg

訓練模型

不同於人和人之間的協作,機器學習的模型在訓練之前是個小白,吃了我們炒的菜:標註資料 後,模型才有可能學會這些資料背後的模式和規律:

async function trainModel(model, inputs, labels) {
  // Prepare the model for training.
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ["mse"],
  });

  const batchSize = 32;
  const epochs = 50;

  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      { name: "Training Performance" },
      ["loss", "mse"],
      { height: 200, callbacks: ["onEpochEnd"] }
    ),
  });
}
複製程式碼

你沒看錯 model.fit 真的是喂 ta 唷,這裡的 batchSize 和 epochs 就是超引數,而前文提到演算法工程師是調參工程師就源於此, 他們大部分時間就真的是在調整這兩個引數。這兩個引數的意思很簡單,batchSize 告訴模型一口吃多少?epochs告訴模型一次吃幾口?老的模型一般怕吃多了噎著,或者不同任務下吃法不對,就像為了下飯可能會一口飯一口菜的吃,為了記住一種美味可能會連吃幾口……

20200407001157.jpg

模型預測

吃完了標註資料這道菜,只要吃法正確,模型就能夠記住這些資料背後的模式和規律,下一步自然是拉模型跟我們協作了:

function testModel(model, inputData, normalizationData) {
  const { inputMax, inputMin, labelMin, labelMax } = normalizationData;

  // Generate predictions for a uniform range of numbers between 0 and 1;
  // We un-normalize the data by doing the inverse of the min-max scaling
  // that we did earlier.
  const [xs, preds] = tf.tidy(() => {
    const xs = tf.linspace(0, 1, 100);
    const preds = model.predict(xs.reshape([100, 1]));

    const unNormXs = xs.mul(inputMax.sub(inputMin)).add(inputMin);

    const unNormPreds = preds.mul(labelMax.sub(labelMin)).add(labelMin);

    // Un-normalize the data
    return [unNormXs.dataSync(), unNormPreds.dataSync()];
  });
複製程式碼

這裡的 model.predict 就是讓模型對 tf.linspace 生成的隨機資料進行預測,輸出就是這資料大概是多少馬力的發動機?因此,告訴模型每加侖行駛里程的資料,模型就能告訴我們這大概是多少馬力的發動機,噠噠!協作完成。

20200407001211.jpg

完整程式碼

/**
 * Get the car data reduced to just the variables we are interested
 * and cleaned of missing data.
 */
async function getData() {
  const carsDataReq = await fetch(
    "https://storage.googleapis.com/tfjs-tutorials/carsData.json"
  );
  const carsData = await carsDataReq.json();
  const cleaned = carsData
    .map((car) => ({
      mpg: car.Miles_per_Gallon,
      horsepower: car.Horsepower,
    }))
    .filter((car) => car.mpg != null && car.horsepower != null);

  return cleaned;
}

/**
 * Define Model
 */
function createModel() {
  // Create a sequential model
  const model = tf.sequential();

  // Add a single input layer
  model.add(tf.layers.dense({ inputShape: [1], units: 1, useBias: true }));
  model.add(tf.layers.dense({ units: 50, activation: "sigmoid" }));
  // Add an output layer
  model.add(tf.layers.dense({ units: 1, useBias: true }));

  return model;
}

/**
 * Convert the input data to tensors that we can use for machine
 * learning. We will also do the important best practices of _shuffling_
 * the data and _normalizing_ the data
 * MPG on the y-axis.
 */
function convertToTensor(data) {
  // Wrapping these calculations in a tidy will dispose any
  // intermediate tensors.

  return tf.tidy(() => {
    // Step 1. Shuffle the data
    tf.util.shuffle(data);

    // Step 2. Convert data to Tensor
    const inputs = data.map((d) => d.horsepower);
    const labels = data.map((d) => d.mpg);

    const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
    const labelTensor = tf.tensor2d(labels, [labels.length, 1]);

    //Step 3. Normalize the data to the range 0 - 1 using min-max scaling
    const inputMax = inputTensor.max();
    const inputMin = inputTensor.min();
    const labelMax = labelTensor.max();
    const labelMin = labelTensor.min();

    const normalizedInputs = inputTensor
      .sub(inputMin)
      .div(inputMax.sub(inputMin));
    const normalizedLabels = labelTensor
      .sub(labelMin)
      .div(labelMax.sub(labelMin));

    return {
      inputs: normalizedInputs,
      labels: normalizedLabels,
      // Return the min/max bounds so we can use them later.
      inputMax,
      inputMin,
      labelMax,
      labelMin,
    };
  });
}

async function trainModel(model, inputs, labels) {
  // Prepare the model for training.
  model.compile({
    optimizer: tf.train.adam(),
    loss: tf.losses.meanSquaredError,
    metrics: ["mse"],
  });

  const batchSize = 32;
  const epochs = 50;

  return await model.fit(inputs, labels, {
    batchSize,
    epochs,
    shuffle: true,
    callbacks: tfvis.show.fitCallbacks(
      { name: "Training Performance" },
      ["loss", "mse"],
      { height: 200, callbacks: ["onEpochEnd"] }
    ),
  });
}

function testModel(model, inputData, normalizationData) {
  const { inputMax, inputMin, labelMin, labelMax } = normalizationData;

  // Generate predictions for a uniform range of numbers between 0 and 1;
  // We un-normalize the data by doing the inverse of the min-max scaling
  // that we did earlier.
  const [xs, preds] = tf.tidy(() => {
    const xs = tf.linspace(0, 1, 100);
    const preds = model.predict(xs.reshape([100, 1]));

    const unNormXs = xs.mul(inputMax.sub(inputMin)).add(inputMin);

    const unNormPreds = preds.mul(labelMax.sub(labelMin)).add(labelMin);

    // Un-normalize the data
    return [unNormXs.dataSync(), unNormPreds.dataSync()];
  });

  const predictedPoints = Array.from(xs).map((val, i) => {
    return { x: val, y: preds[i] };
  });

  const originalPoints = inputData.map((d) => ({
    x: d.horsepower,
    y: d.mpg,
  }));

  tfvis.render.scatterplot(
    { name: "Model Predictions vs Original Data" },
    {
      values: [originalPoints, predictedPoints],
      series: ["original", "predicted"],
    },
    {
      xLabel: "Horsepower",
      yLabel: "MPG",
      height: 300,
    }
  );
}

async function run() {
  // Load and plot the original input data that we are going to train on.
  const data = await getData();
  const values = data.map((d) => ({
    x: d.horsepower,
    y: d.mpg,
  }));

  tfvis.render.scatterplot(
    { name: "Horsepower v MPG" },
    { values },
    {
      xLabel: "Horsepower",
      yLabel: "MPG",
      height: 300,
    }
  );

  // More code will be added below
  // Create the model
  const model = createModel();
  tfvis.show.modelSummary({ name: "Model Summary" }, model);

  // Convert the data to a form we can use for training.
  const tensorData = convertToTensor(data);
  const { inputs, labels } = tensorData;

  // Train the model
  await trainModel(model, inputs, labels);
  console.log("Done Training");

  // Make some predictions using the model and compare them to the
  // original data
  testModel(model, data, tensorData);
}

document.addEventListener("DOMContentLoaded", run);
複製程式碼

這就完成了整個買菜(獲取資料)、洗菜(處理資料)、炒菜(處理資料)、獵頭(尋找/抄/定義模型)、投食(訓練模型)、協作(模型預測)的過程,全部都是在瀏覽器內完成(Tensorflow.js 已經加入了WASM的加速能力,未來會支援 WebNN 的加速能力),全部都是前端技術,你說前端智慧化是否已經到來?

What`s next

  • 還在釋出上線的時候盯屏看各種指標?把 日誌和指標 組織成示例中 每加侖行駛里程和發動機馬力 的格式,拿上面的程式碼訓練個模型,寫個Chrome外掛來盯屏吧。
  • 把後續看完,系統學習智慧化,在前端領域進行人機協同的程式設計。
  • 用本系列介紹的原理方法,去 www.tensorflow.org/resources/m… 抄更多模型。

相關文章