WinForm通用自動更新AutoUpdater項目實戰

目前我們做的上位機項目還是以Winform為主,在實際應用過程中,可能還會出現一些細節的修改。對於這種情況,如果上位機帶有自動更新功能,我們只需要將更新后的應用程序打包放在指定的路徑下,可以讓用戶自己來進行更新使用,會大大增加項目的便捷性。

 

01.自動更新整體思路

今天給大家介紹一下如何基於C#實現WinForm自動更新的一種方式,這種方式長期應用在項目中,提供了很多幫助,也節約了大量的時間成本,並且也使用在CMSPro軟件中,整體流程如下圖所示:

圖表 1自動更新流程

 

02.實現說明

通過上圖,可以發現這種方式是基於打包文件的方式實現的,好處在於整體打包下載,即使中途出現網絡中斷也不會有任何影響,當然相比於那種單個文件更新的方式,可能每次耗時會多一些,但是由於更新並不是一個頻繁操作的過程,這個時間是可以接受的。

(1)首先對於項目是否啟用自動更新,是通過配置的方式實現的,在實際開發中,可以使用手動更新和自動更新兩種方式,當啟用自動更新時,每次啟動應用程序都會與服務器版本號做下比較,判斷是否執行自動更新的流程。

圖表 2自動更新界面

 

(2)對於手動更新,可以通過點擊,彈出一個手動更新窗體,如下圖所示:

圖表 3手動更新界面

 

(3)對於服務器路徑、本地版本號等信息都是通過本地配置文件存儲的,因此本地需要有一個LocalVersion的配置文件,具體用什麼形式,可以自由選擇,Ini、Txt、Xml、Json都可以,如下圖所示:

圖表 4本地配置文件參考

 

(4)服務器側也會有一個配置文件,形式自由選擇,應該包含以下信息:當前服務器版本號、最新版本的程序包、該版本是否更新、該版本更新內容等信息,同時如果有新版本,應該將新版本的文件放到指定路徑下,保證最新版本包的這個路徑是有效路徑。

圖表 5服務器配置文件參考

 

(5)上位機通過將服務器的最新版本號與本地的版本號做對比,如果服務器的版本號較大,說明服務器有更新版本,因此,會根據最新版本包的地址進行下載,這裏採用的是zip文件,下載過程根據網絡及實際情況可能會耗時,因此上位機側應該做個進度條,讓用戶知道下載的進度情況,同時對於每一步的狀態也應該通過圖標的方式來進行显示,讓用戶明確更新的進度情況。

圖表 6自動更新流程

 

(6)更新完成后,系統會自動重啟新的應用程序,可以看到軟件從之前的5.3.5版本升級到最新的6.0.0版本。

圖表 7更新結果

03.整體總結

本文主要工控上位機進行自動更新的流程做了一個整體介紹,主要是介紹流程為主,給大家分享一下實現的整體思路,畢竟每個人的實現方式都可能有所不同,大家也可以在此基礎上增加一個新的功能,給自己的上位機軟件增加一點特色的同時,也給自己提供了便捷一下升級的過程的話,可以通過關注本公眾號:dotNet工控上位機,併發送關鍵詞:CMSPro,下載之後安裝運行,便會直接進入版本升級的過程。

公眾號:thinger_swj

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

[computer graphics]簡單光照模型(Phong和Blinn-Phong)和明暗處理

簡單光照模型(Phong和Blinn-Phong)和明暗處理

支持點光源和平行光,是一種簡單光照模型,它將光照分解成了三個部分,分別為

  • 漫反射
  • 鏡面反射
  • 環境光

如圖所示,是一個簡單的幾何模型。

  • \(L\)是光源方向
  • \(N\)是法線方向
  • \(R\)是反射方向
  • \(V\)是視線方向
  • \(H\)\(L\)\(V\)的平分
  • 所有向量都是單位向量

理想漫反射

當光源來自一個方向時,漫反射均勻地向各個方向傳播,與視點無關,是由物體表面粗糙不平引起的,漫反射的空間分佈是均勻的,也就是說不論從哪個方向看去,同一個點的漫反射光強都是一樣的。物體上的點\(P\),法向量為\(N\),入射光強度為\(I_p\)\(L\)\(P\)指向光源的方向。如果所有所有的向量都是單位向量,那麼有

\[I_d = I_pK_d\cdot(L\cdot N) \]

其中\(K_d=(K_{dr},K_{dg},K_{db})\)這三個分量分別是RGB三原色的漫反射係數,可以反應物體的顏色。同樣的\(I_p=(I_r,I_g,I_b)\)可以通過分量來設置光源的顏色。

鏡面反射

對於理想鏡面,反射光集中在一個方向,並遵守反射定律。對於一般的光滑表面,反射光則集中在一個範圍內,且反射定律決定的方向光強最大。所以從不同位置觀察到的鏡面反射光強不同。鏡面反射光可表示為

\[I_s = I_pK_s(R\cdot V)^{n} \]

\(R \cdot V\)計算的是反射方向和視線方向的夾角,夾角越小,強度越大。\(n\)是反射指數,反應了物體的表面的光滑程度,一般1-2000。\(n\)越大約光滑,因為n越大,例如2000,那麼當夾角很小時,例如很接近1,如0.9,但是經過2000乘方,就變得很小了,這意味着只有無限接近反射方向,才能看到高光,其他方向不行,這就表示物體很光滑。反過來,\(n\)很小那麼移動一點角度,也能看到衰弱的高光,所以光斑會比較明顯。

在鏡面反射模型中,最終要的是計算R的方向,\(R\)可以通過入射方向和法線方向計算出來

因為這裏的向量都是單位向量,只有方向不一致

\[\begin{aligned} ||L||\cos\theta &= ||M||=\cos\theta\\ &M和N的方向一致\\ R &= -L+2M \\ &=2N\cos\theta-L\\ &=2N\cdot(N\cdot L)-L \end{aligned} \]

高光區域只反映光源的顏色,漫反射才能設定物體的顏色。

環境光

光源間接對物體施加的明暗影響,在物體和環境之間多次反射。在簡單光照模型中進行了簡化,通常用一個常數來模擬環境光

\[I = I_aK_a \]

\(I_a\)是環境光強,\(K_a\)為物體對環境光的反射係數。

Phong模型

\[I = I_aK_a +I_pK_d\cdot(L\cdot N)+I_pK_s(R\cdot V)^{n} \]

Phong模型是上述三種因素的疊加,其中\(R\)的計算比較費時,需要對每一點計算一次\(R\)的值。

Blinn-Phong模型

由於Phong模型計算較為耗時,後來提出了一種對Phong模型的修改,Blinn-Phong模型。
假設:

  1. 光源在無窮遠處,光線的方向L為常數(這就意味着,對物體上所有點來說,光線的方向都是一致的,正常情況應該是光源到點的向量,每個點的光照方向都不一致)
  2. 視點在無窮遠處,視線的防線V為常數(這個同理)
  3. 此模型針對高光部分進行了修改,\(R\cdot V\)的計算用\(H\cdot N\)近似,其中\(H=(L+V)/||L+V||\),也就是\(L\)\(V\)的平分向量。當\(V\)接近\(R\)的時候,\(H\)也接近\(N\),符號高光的規律。對於所有點,\(H\)只需計算一次。

所以Blinn-Phong模型的可以表示成:

\[I = I_aK_a +I_pK_d\cdot(L\cdot N)+I_pK_s(H\cdot N)^{n} \]

(圖片中應該採用了明暗處理,不僅是光照模型)

明暗處理

如今的物體大多數用多邊形表示,一個多邊形的法線方向一致,因此一個多邊形內部的像素相同,而在鄰接出可能會有突變,感覺不連續。為了讓過度平滑,基本思想是:對多邊形的頂點計算合適的光強度,在內部進行均勻插值。其中有兩種主要的做法:

  • 計算物體表面多邊形頂點的光強,然後插值,求多邊形內部光強。
  • 對內部點的法向量進行插值,而頂點的法向量用相鄰多邊形的法向量的平均值得到。

Gouraud明暗處理(雙線性光強插值)

基本算法

  1. 計算多邊形頂點的平均法向量
  2. 用Phong模型計算頂點的平均強度
  3. 插值計算離散邊上的各點光強
  4. 插值計算多邊形區域內的各點光強

計算速度比簡單光照模型有了很大的提高,解決了顏色突變問題,但是鏡面反射效果不理想。

Phong明暗處理(雙線性法向量插值)

和Gouraud方法基本類似,只不過是對法向量插值。多邊形頂點的法向量用相鄰多邊形的法向量的平均值。而內部每個點都要計算法向量,用頂點的法向量插值得到。
這種做法效果好,可以產生正確的高光,但是計算量很大。

  • [1]維基百科
  • [2]計算機圖形學基礎教程 胡事民

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

AntD框架的upload組件上傳圖片時使用customRequest方法自定義上傳行為

本次做後台管理系統,採用的是 AntD 框架。涉及到圖片的上傳,用的是AntD的 upload 組件。

我在上一篇文章《前端AntD框架的upload組件上傳圖片時遇到的一些坑》中講到:AntD 的 upload 組件有很多坑,引起了很多人的關注。折騰過的人,自然明白其中的苦楚。

今天這篇文章,我們繼續來研究 AntD 的 upload 組件的另一個坑。

備註:本文寫於2020-06-11,使用的 antd 版本是 3.13.6。

使用 AntD 的 upload 組件做圖片的上傳,效果演示

因為需要上傳多張圖片,所以採用的是照片牆的形式。上傳成功后的界面如下:

(1)上傳中:

(2)上傳成功:

(3)圖片預覽:

代碼實現

首先,你需要讓後台同學提供好圖片上傳的接口。上一篇文章中,我們是把接口調用直接寫在了 <Upload> 標籤的 action 屬性當中。但如果你在調接口的時候,動作很複雜(比如根據業務要求,需要連續調兩個接口才能上傳圖片,或者在調接口時還要做其他的事情),這個 action 方法就無法滿足需求了。那該怎麼做呢?

好在 AntD 的 upload 組件給我們提供了 customRequest這個方法:

關於customRequest 這個方法, AntD 官方並沒有給出示例,他們只是在 GitHub 上給出了這樣一個簡短的介紹:

但這個方法怎麼用呢?用的時候,會遇到什麼問題呢?AntD 官方沒有說。我在網上搜了半天,也沒看到比較完整的、切實可行的 Demo。我天朝地大物博,網絡資料浩如煙海,AntD 可是口口聲聲被人們號稱是天朝最好用的管理後台的樣式框架。可如今,卻面臨這樣的局面。我看着你們,滿懷羡慕。

既然如此,那我就自己研究吧。折騰了一天,總算是把 customRequest 的坑踩得差不多了。

啥也不說了,直接上代碼。

採用 AntD框架的 upload 組件的 customRequest 方法,自定義上傳行為。核心代碼如下:

import React, { PureComponent } from 'react';
import { Button, Card, Form, message, Upload, Icon, Modal, Row, Col } from 'antd';
import { connect } from 'dva';
import { queryMyData, submitData } from '../api';
import { uploadImage } from '../../utils/wq.img.upload';

import styles from '../../utils/form.less';

const FormItem = Form.Item;

@Form.create()
export default class PicturesWall extends PureComponent {
  constructor(props) {
    super(props);
    const { id } = this.props.match.params;
    this.state = {
      id,
      img: undefined, // 從接口拿到的圖片字段
      imgList: [], // 展示在 antd圖片組件上的數據
      previewVisible: false,
      previewImage: '',
    };
  }

  componentDidMount() {
    const { id } = this.state;
    id && this.queryData();
  }

  // 調接口,查詢已有的數據
  queryData() {
    const { id } = this.state;
    queryMyData({
      id,
    })
      .then(({ ret, data }) => {
        if (ret == 0 && data && data.list && data.list.length) {
          const item = data.list[0];

          const img = data.img;
          const imgList = item.img
            ? [
              {
                uid: '1', // 注意,這個uid一定不能少,否則展示失敗
                name: 'hehe.png',
                status: 'done',
                url: img,
              },
            ]
            : [];

          this.setState({
            img,
            imgList,
          });
        } else {
          return Promise.reject();
        }
      })
      .catch(() => {
        message.error('查詢出錯,請重試');
      });
  }

  handleCancel = () => this.setState({ previewVisible: false });

  // 方法:圖片預覽
  handlePreview = (file) => {
    console.log('smyhvae handlePreview:' + JSON.stringify(file));
    this.setState({
      previewImage: file.url || file.thumbUrl,
      previewVisible: true,
    });
  };

  // 參考鏈接:https://www.jianshu.com/p/f356f050b3c9
  handleBeforeUpload = (file) => {
    console.log('smyhvae handleBeforeUpload file:' + JSON.stringify(file));
    console.log('smyhvae handleBeforeUpload file.file:' + JSON.stringify(file.file));
    console.log('smyhvae handleBeforeUpload file type:' + JSON.stringify(file.type));

    //限製圖片 格式、size、分辨率
    const isJPG = file.type === 'image/jpeg';
    const isJPEG = file.type === 'image/jpeg';
    const isGIF = file.type === 'image/gif';
    const isPNG = file.type === 'image/png';
    const isLt2M = file.size / 1024 / 1024 < 1;
    if (!(isJPG || isJPEG || isPNG)) {
      Modal.error({
        title: '只能上傳JPG、JPEG、PNG格式的圖片~',
      });
    } else if (!isLt2M) {
      Modal.error({
        title: '圖片超過1M限制,不允許上傳~',
      });
    }
    return (isJPG || isJPEG || isPNG) && isLt2M;
  };

  // checkImageWH  返回一個promise  檢測通過返回resolve  失敗返回reject阻止圖片上傳
  checkImageWH(file) {
    return new Promise(function (resolve, reject) {
      let filereader = new FileReader();
      filereader.onload = (e) => {
        let src = e.target.result;
        const image = new Image();
        image.onload = function () {
          // 獲取圖片的寬高
          file.width = this.width;
          file.height = this.height;
          resolve();
        };
        image.onerror = reject;
        image.src = src;
      };
      filereader.readAsDataURL(file);
    });
  }

  // 圖片上傳
  doImgUpload = (options) => {
    const { onSuccess, onError, file, onProgress } = options;

    // start:進度條相關
    // 偽裝成 handleChange裏面的圖片上傳狀態
    const imgItem = {
      uid: '1', // 注意,這個uid一定不能少,否則上傳失敗
      name: 'hehe.png',
      status: 'uploading',
      url: '',
      percent: 99, // 注意不要寫100。100表示上傳完成
    };

    this.setState({
      imgList: [imgItem],
    }); // 更新 imgList
    // end:進度條相關

    const reader = new FileReader();
    reader.readAsDataURL(file); // 讀取圖片文件

    reader.onload = (file) => {
      const params = {
        myBase64: file.target.result, // 把 本地圖片的base64編碼傳給後台,調接口,生成圖片的url
      };

      // 上傳圖片的base64編碼,調接口后,返回 imageId
      uploadImage(params)
        .then((res) => {
          console.log('smyhvae doImgUpload:' + JSON.stringify(res));
          console.log('smyhvae 圖片上傳成功:' + res.imageUrl);

          const imgItem = {
            uid: '1', // 注意,這個uid一定不能少,否則上傳失敗
            name: 'hehe.png',
            status: 'done',
            url: res.imageUrl, // url 是展示在頁面上的絕對鏈接
            imgUrl: res.imageUrl, // imgUrl 是存到 db 里的相對鏈接
            // response: '{"status": "success"}',
          };

          this.setState({
            imgList: [imgItem],
          }); // 更新 imgList
        })
        .catch((e) => {
          console.log('smyhvae 圖片上傳失敗:' + JSON.stringify(e || ''));
          message.error('圖片上傳失敗,請重試');
        });
    };
  };

  handleChange = ({ file, fileList }) => {
    console.log('smyhvae handleChange file:' + JSON.stringify(file));
    console.log('smyhvae handleChange fileList:' + JSON.stringify(fileList));

    if (file.status == 'removed') {
      this.setState({
        imgList: [],
      });
    }
  };

  submit = (e) => {
    e.preventDefault();

    this.props.form.validateFields((err, fieldsValue) => {
      if (err) {
        return;
      }

      const { id, imgList } = this.state;

      const tempImgList = imgList.filter((item) => item.status == 'done'); // 篩選出 status = done 的圖片
      const imgArr = [];
      tempImgList.forEach((item) => {
        imgArr.push(item.imgUrl);
        // imgArr.push(item.url);
      });

      submitData({
        id,
        img: imgArr[0] || '', // 1、暫時只傳一張圖片給後台。如果傳多張圖片,那麼,upload組件需要進一步完善,比較麻煩,以後有需求再優化。2、如果圖片字段是選填,那就用空字符串兜底
      })
        .then((res) => {
          if (res.ret == 0) {
            message.success(`${id ? '修改' : '新增'}成功,自動跳轉中...`);

          } else if (res.ret == 201 || res.ret == 202 || res.ret == 203 || res.ret == 6) {
            return Promise.reject(res.msg);
          } else {
            return Promise.reject();
          }
        })
        .catch((e) => {
          message.error(e || '提交失敗,請重試');
        });
    });
  };

  render() {
    const { id, imgList } = this.state;
    console.log('smyhvae render imgList:' + JSON.stringify(imgList));
    const { getFieldDecorator } = this.props.form;
    const formItemLayout = {
      labelCol: { span: 3 },
      wrapperCol: { span: 10 },
    };
    const buttonItemLayout = {
      wrapperCol: { span: 10, offset: 3 },
    };

    const uploadButton = (
      <div>
        <Icon type="plus" />
        <div className="ant-upload-text">Upload</div>
      </div>
    );

    return (
      <Card title={id ? '修改信息' : '新增信息'}>
        <Form onSubmit={this.submit} layout="horizontal">

          {/* 新建圖片、編輯圖片 */}
          <FormItem label="圖片" {...formItemLayout}>
            {getFieldDecorator('img', {
              rules: [{ required: false, message: '請上傳圖片' }],
            })(
              <Upload
                action="2"
                customRequest={this.doImgUpload}
                listType="picture-card"
                fileList={imgList}
                onPreview={this.handlePreview}
                beforeUpload={this.handleBeforeUpload}
                onChange={this.handleChange}
              >
                {imgList.length >= 1 ? null : uploadButton}
              </Upload>
            )}
          </FormItem>
          <Row>
            <Col span={3} />
            <Col span={18} className={styles.graytext}>
              注:圖片支持JPG、JPEG、PNG格式,小於1M,最多上傳1張
            </Col>
          </Row>

          <FormItem {...buttonItemLayout}>
            <Button type="primary" htmlType="submit">
              提交
            </Button>
          </FormItem>
        </Form>

        {/* 圖片點開預覽 */}
        <Modal visible={this.state.previewVisible} footer={null} onCancel={this.handleCancel}>
          <img alt="example" style={{ width: '100%' }} src={this.state.previewImage} />
        </Modal>
      </Card>
    );
  }
}

參考鏈接

注意file的格式:https://www.lmonkey.com/t/oREQA5XE1

Demo在線演示:

  • https://stackoverflow.com/questions/58128062/using-customrequest-in-ant-design-file-upload

fileList 格式在線演示:

  • https://stackoverflow.com/questions/51514757/action-function-is-required-with-antd-upload-control-but-i-dont-need-it

ant design Upload組件的使用總結:https://www.jianshu.com/p/0aa4612af987

antd上傳功能的CustomRequest:https://mlog.club/article/3832743

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

大話性能測試系列(1)- 性能測試的基本概念

如果你對性能測試感興趣,但是又不熟悉理論知識,可以看下面的系列文章

https://www.cnblogs.com/poloyy/category/1620792.html

 

學習前的認知

我們在學習性能測試之前,需要有個新的認識:性能測試,不再是像功能測試一樣單純的找 Bug,而是去找性能指標

 

轉變思維

  • 在做功能測試、自動化測試的時候,我們基本都是依託界面進行測試,也稱 GUI 測試,我們的目的就是為了跑通功能、程序,並成功找到 Bug
  • 但在做性能測試的時候,我們大部分是 headless 模式(所謂的:無頭,無界面模式),目的不再是單純的為了找到 Bug,而是要分析性能指標等等(後續講到)

 

性能測試的時間一般會比自動化、功能測試長,為啥?

  • 因為性能測試的步驟跟自動化、功能測試的步驟不一樣,比如說前期的準備(了解系統,環境搭建),後期的壓力測試(7*24h)等等
  • 在後面,我們通過講述性能測試步驟來仔細了解

 

性能測試一定要工具,手工不行嗎?

  • 性能測試是模擬系統在被很多很多用戶同時使用時,系統能不能正常使用和提供服務
  • 重點:很多很多用戶
  • 功能測試:一個人點點點就知道功能通不通,有沒有 Bug 了
  • 性能測試:用手工的話,可以模擬幾個、十幾個用戶,但是當需要模擬上千萬個用戶時,手工又怎麼模擬數據量多的場景呢?
  • 類比,吃飯場景:一個人可以吃好幾碗,但是叫你吃幾百碗是不可能的
  • 結論:工具就可以模擬大數據量的場景,可以做到人做不到的事情

 

大數據量測試是性能測試嗎?

大數據量測試

簡單理解:一個接口返回的數據比較多(假設:不使用分頁,把所有數據同時返回)

 

結論

  • 返回大數據量的接口的響應時間會變長
  • 這麼大的數據量,我們需要考慮:網絡傳輸數據、服務器查詢這些數據、服務器處理這些數據等等分別需要多少時間
  • 這已經跟響應時間掛鈎,所以已經屬於性能測試的範圍,但不歸納於性能分析範圍

 

大數據測試是性能測試嗎?

大數據測試的功能屬於功能測試哦

 

性能測試過程發現問題需要立即提交嗎?

在性能測試過程中發現一些問題,假設定位到某一段代碼有問題,可以截圖提交 Bug 給開發,但這並不是我們性能測試的最終目的,最終目的是找出性能指標

 

有哪些性能指標?

  • 比如說響應時間:10個人、100個人 、1000個人 、10000個人向服務器發起請求,服務器響應請求的平均響應時間是多少,這就是一個指標
  • 又好比TPS:服務器在當前的配置下,不同用戶數發起請求,服務器的 TPS 處理能力是多少,這也是一個指標
  • 後續詳細介紹

 

性能測試中發現的 Bug 

  • 性能測試過程中發現的 Bug 屬於一個衍生品,並不是最終得到的結果
  • 但像功能測試,最終目的就是為了找出 Bug

 

關於這個問題的總結

  • 做性能測試,當數據量變大后,會出現連接超時、連接拒絕、500、502異常問題;在性能測試中,這些異常問題基本都會出現的,但不會去立即提 Bug
  • 對於性能測試工程師,我們要做的是分析為什麼在當前數據量下會出現連接超時、連接拒絕,響應時間超時、服務器異常等異常問題
  • 這就需要我們去分析性能瓶頸,並不會單獨去某個異常問題出現在哪裡,而是分析為什麼會出現這個異常問題,分析的是服務器或者是代碼,而不是讓開發人員馬上來修復這些異常問題

 

我們常說的壓測是指壓力測試嗎?

  • 並不是,而是指負載測試,一般都是為了找出系統的最大負載量
  • 就好像你老闆說:你去壓測下,看看系統能支撐多少用戶同時訪問我們的系統

 

什麼是性能測試?

狹義理解

  • 通過工具,找出或獲得系統在不同工況下的性能指標值
  • 性能測試過程中,重點是找出性能指標,而不再是找出 Bug,
  • 性能測試的產出絕對不只是 Bug

 

場景類比

跑步100米,用時多少?運動員的心跳、步伐頻率是多少?

  1. 跑步100米:業務場景
  2. 用時多少:響應時間
  3. 運動員的心跳、步伐:性能指標值

性能指標值和響應時間是否滿足當前業務場景的最低要求(合格線)

 

什麼時候能找出性能指標值

假設當前有一個業務

電商系統,下單業務,目前還不知道系統支持多少人同時下單,那麼我們需要找到服務器能正常支持多少人同時下單

 

性能測試初始階段(第一次做)

  • 先把基礎的性能指標值找出來(第一次性能測試也叫做基準測試)
  • 比如:100個人同時下單系統正常,但120個人同時下單就會出現部分請求的響應時間超長,連接異常
  • 那麼100-120範圍內的某個值就是當前服務器能達到的性能指標值(基準值)

 

版本迭代,進行第二次做性能測試,重新跑一遍之前的性能腳本

  • 又會得到一些性能指標值,對比上個版本的性能指標值,看是否有優化(性能變化)
  • 假設這個時候120個人同時下單是正常的,150個人才有異常,那麼接口已經有優化了

 

假設公司是從0開始做性能測試

  • 第一階段:做好性能測試,得到性能指標值
  • 第二階段:假設性能比之前差,哪些性能指標值不滿足預期值,就需要分析是哪裡有問題

 

廣義理解

  • 只要與服務器性能指標相關的測試都屬於性能測試
  • 比如:響應時間、併發用戶數、服務器處理能力、吞吐量等性能指標
  • 負載測試、壓力測試、容量測試、可靠性測試都屬於性能測試
  • 通常嘴巴上說的做性能測試就是廣義的性能測試,它包括了很多內容,並不只是針對某一個測試類型

 

“官方”解釋

以下含義來源高老的解釋,比較“官方”的術語

  1. 性能測試針對系統的性能指標,建立性能測試模型
  2. 制定性能測試方案
  3. 制定監控策略
  4. 在場景條件下執行性能場景
  5. 分析判斷性能瓶頸並調優
  6. 最終得出性能結果來評估系統的性能指標是否滿足既定值

其實也算是一個簡潔描述的性測試流程了

 

注意

  • 性能測試不像自動化測試那樣很多東西大家都是公認的,性能測試沒有一套標準的知識體系,只能說是相似的
  • 基本每個人都有自己的一套知識體系,就好像高老也會說他給性能測試的定義很大可能會被轟炸一樣
  • 只要屬於自己的知識體系建立起來了,那麼就能助力你正確的完成性能測試
  • 不用太過糾結於哪個人對性能測試概念的解釋是最準確的

目前博主是正在學習性能測試的小白一枚,希望通過通俗簡單的術語來學懂性能測試,打造屬於自己的知識體系,歡迎大家進群與我溝通(870155189)

 

 什麼是負載測試?

概念

  • 逐步增加系統負載,測試系統性能變化,並最終確定系統所能承受的最大負載量
  • 通俗理解:看看你幾斤幾兩

 

如何增加負載

通過增加“用戶數”,就是常說的併發數

 

場景類比

天平秤,稱東西的時候,需要逐步加砝碼,最終達到砝碼和物品重量的平衡點,因為它不可能一下子就達到平衡點【好比不可能一下子找到系統能承受的最大負載量】

  • 稱東西:業務場景
  • 加砝碼:逐步加壓
  • 達到平衡點:找到最大負載量

 

實際場景

  • 有一個業務,增加到40個人的時候,服務器還能正常使用,沒有異常
  • 當你增加到50個人的時候,服務器已經開始有異常了,那麼就能確定40-50之間某個值就是系統所能承受的最大負載量【出現性能拐點,找到了服務器性能瓶頸的範圍值】
  • 最後減小加壓梯度(比如:從40個人開始每次增加1個人、2個人),確認最大負載量【確認性能拐點】

 

服務器又有哪些可能會出現的異常呢

  • 響應時間超長:正常服務器處理請求時間是 1s,但現在變成3s – 5s
  • 服務報錯:無法同時正常響應多個請求
  • 服務器宕機:系統完全用不了

 

什麼是壓力測試?

概念

  • 在較大的性能壓力下,持續運行一個比較長的時間,看看系統服務是否正常及系統資源的利用率情況
  • 通俗理解:鴨梨山大!
  • 關鍵字:較大壓力 + 較長時間
  • 注意:不是滿負荷壓力哦

 

場景類比

問:大家什麼時候會覺得工作壓力大?

答:996、007;因為你不會覺得955壓力山大吧

結論:所以在我們日常工作中,長時間工作強度高,才會覺得壓力大;如果你一周就加班一天也說壓力大…(那就別干這一行了)

 

壓力測試用來幹嘛的

測試系統的穩定性

 

類比

工作壓力大,你還能堅持下去(那穩定性肯定好吧),那如果你很快就離職了(那穩定性肯定差,都宕機罷工了)

 

什麼時候會做壓力測試

  • 生產環境下,系統隔三差五的出現不穩定的情況
  • 這個時候,就需要通過壓力測試去測試系統的穩定性情況

 

啥情況算不穩定?穩定性差?

隔三差五的出現下面的情況

  • 服務異常:響應錯誤、響應時間超時等
  • 服務器出現異常:宕機

 

怎麼分析是服務異常還是服務器異常 

  • 如果所有請求都是一片紅,應用程序發送的所有請求都報紅,就是服務器出現了異常
  • 如果有些請求偶爾成功響應,偶爾又失敗,則是服務異常,出現不穩定的情況

 

如何取壓力值

  • 在負載測試中,我們確認了系統所能承受的最大負載量
  • 壓力值 < 最大負載量,一般取80%左右

 

靈魂拷問

負載測試一般時間比較短,壓力測試時間比較長,持續運行時間短就能正常使用,但持續運行時間長就可能崩掉了,這是什麼原因呢?

 

場景類比

  • 栗子一:電腦保持開機狀態很長時間,會逐漸變卡,因為內存的東西會越來越多,得不到有效的回收, 就會越來越卡
  • 栗子二:當你經常工作壓力很大,且你的心理所能承受的壓力逐漸達到最大值時,你就可能會選擇離職

 

總結

壓力測試長時間運行,可能會逐漸增加系統的內存佔用空間,若得不到有效的內存回收,當達到內存最大值時,系統就會崩掉

 

壓力測試持續運行時間要多久?

  • 標準性能測試裏面,一般是7*24小時,或者是它的倍數
  • 但是實際工作中,並不會這麼久,否則成本太高
  • 所以會以小時為單位,比如:4個小時、8個小時…晚上下班之後做,第二天早上上班看結果

 

先負載測試還是壓力測試?

  • 先負載測試
  • 負載測試可以找到服務器性能瓶頸的範圍值,若生產環境中系統穩定性較差,再做壓力測試
  • 所以壓力測試是可做可不做的

 

什麼是可靠性測試?

概念

  • 在給定的一定的業務壓力下,持續運行一段時間,查看系統是否穩定
  • 關鍵字:是否穩定,一定業務壓力
  • 注意:不是較大壓力哦

 

業務場景栗子

電商秒殺場景,幾十個商品幾十萬個人同時秒殺搶購

 

如何理解可靠性測試

  1. 編寫性能腳本:假設一秒內有一萬個人同時發起請求
  2. 有壓力嗎?,一萬個人同時發起請求
  3. 但是持續時間,不像壓力測試一樣需要持續一段時間
  4. 目的是為了驗證當這麼多人同時發起請求時,成功秒殺的用戶能否繼續完成後續下單付款等操作【一定業務壓力下,系統是否穩定運行】

 

什麼是容量測試?

概念

  • 在一定的軟、硬件條件下,在數據庫不同數據量級數據量的情況下,對系統中讀/寫比較多的業務進行測試,從而獲得不同數據量級下的性能指標值
  • 關鍵字:不同數據量級

 

數據庫數據量對性能測試結果有沒有影響?

肯定有

  • 比如數據庫已經有幾百條數據和幾百萬條數據,查詢的速度肯定不一樣,所以肯定會影響性能測試結果
  • 數據量級的差異,會影響TPS、響應時間、網絡等

 

場景類比

從一袋米中找一個綠豆,和一碗米中找一個綠豆,找的時間肯定是千差萬別的

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Flink 如何分流數據

  • 場景
  • 分流方式
  • 如何分流
    • 使用Filter分流
    • 使用Split分流
    • 使用Side Output分流

場景

獲取流數據的時候,通常需要根據所需把流拆分出其他多個流,根據不同的流再去作相應的處理。

舉個例子:創建一個商品實時流,商品有季節標籤,需要對不同標籤的商品做統計處理,這個時候就需要把商品數據流根據季節標籤分流。

分流方式

  • 使用Filter分流
  • 使用Split分流
  • 使用Side Output分流

如何分流

先模擬一個實時的數據流

import lombok.Data;
@Data
public class Product {
    public Integer id;
    public String seasonType;
}

自定義Source

import common.Product;
import org.apache.flink.streaming.api.functions.source.SourceFunction;

import java.util.ArrayList;
import java.util.Random;

public class ProductStremingSource implements SourceFunction<Product> {
    private boolean isRunning = true;

    @Override
    public void run(SourceContext<Product> ctx) throws Exception {
        while (isRunning){
            // 每一秒鐘產生一條數據
            Product product = generateProduct();
            ctx.collect(product);
            Thread.sleep(1000);
        }
    }

    private Product generateProduct(){
        int i = new Random().nextInt(100);
        ArrayList<String> list = new ArrayList();
        list.add("spring");
        list.add("summer");
        list.add("autumn");
        list.add("winter");
        Product product = new Product();
        product.setSeasonType(list.get(new Random().nextInt(4)));
        product.setId(i);
        return product;
    }
    @Override
    public void cancel() {

    }
}

輸出:

使用Filter分流

使用 filter 算子根據數據的字段進行過濾。

import common.Product;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import source.ProductStremingSource;

public class OutputStremingDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Product> source = env.addSource(new ProductStremingSource());

        // 使用Filter分流
        SingleOutputStreamOperator<Product> spring = source.filter(product -> "spring".equals(product.getSeasonType()));
        SingleOutputStreamOperator<Product> summer = source.filter(product -> "summer".equals(product.getSeasonType()));
        SingleOutputStreamOperator<Product> autumn  = source.filter(product -> "autumn".equals(product.getSeasonType()));
        SingleOutputStreamOperator<Product> winter  = source.filter(product -> "winter".equals(product.getSeasonType()));
        source.print();
        winter.printToErr();

        env.execute("output");
    }
}

結果輸出(紅色為季節標籤是winter的分流輸出):

使用Split分流

重寫OutputSelector內部類的select()方法,根據數據所需要分流的類型反正不同的標籤下,返回SplitStream,通過SplitStream的select()方法去選擇相應的數據流。

只分流一次是沒有問題的,但是不能使用它來做連續的分流。

SplitStream已經標記過時了

public class OutputStremingDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Product> source = env.addSource(new ProductStremingSource());

        // 使用Split分流
        SplitStream<Product> dataSelect = source.split(new OutputSelector<Product>() {
            @Override
            public Iterable<String> select(Product product) {
                List<String> seasonTypes = new ArrayList<>();
                String seasonType = product.getSeasonType();
                switch (seasonType){
                    case "spring":
                        seasonTypes.add(seasonType);
                        break;
                    case "summer":
                        seasonTypes.add(seasonType);
                        break;
                    case "autumn":
                        seasonTypes.add(seasonType);
                        break;
                    case "winter":
                        seasonTypes.add(seasonType);
                        break;
                    default:
                        break;
                }
                return seasonTypes;
            }
        });
        DataStream<Product> spring = dataSelect.select("machine");
        DataStream<Product> summer = dataSelect.select("docker");
        DataStream<Product> autumn = dataSelect.select("application");
        DataStream<Product> winter = dataSelect.select("middleware");
        source.print();
        winter.printToErr();

        env.execute("output");
    }
}

使用Side Output分流

推薦使用這種方式

首先需要定義一個OutputTag用於標識不同流

可以使用下面的幾種函數處理流發送到分流中:

  • ProcessFunction
  • KeyedProcessFunction
  • CoProcessFunction
  • KeyedCoProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction

之後再用getSideOutput(OutputTag)選擇流。

public class OutputStremingDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Product> source = env.addSource(new ProductStremingSource());

        // 使用Side Output分流
        final OutputTag<Product> spring = new OutputTag<Product>("spring");
        final OutputTag<Product> summer = new OutputTag<Product>("summer");
        final OutputTag<Product> autumn = new OutputTag<Product>("autumn");
        final OutputTag<Product> winter = new OutputTag<Product>("winter");
        SingleOutputStreamOperator<Product> sideOutputData = source.process(new ProcessFunction<Product, Product>() {
            @Override
            public void processElement(Product product, Context ctx, Collector<Product> out) throws Exception {
                String seasonType = product.getSeasonType();
                switch (seasonType){
                    case "spring":
                        ctx.output(spring,product);
                        break;
                    case "summer":
                        ctx.output(summer,product);
                        break;
                    case "autumn":
                        ctx.output(autumn,product);
                        break;
                    case "winter":
                        ctx.output(winter,product);
                        break;
                    default:
                        out.collect(product);
                }
            }
        });

        DataStream<Product> springStream = sideOutputData.getSideOutput(spring);
        DataStream<Product> summerStream = sideOutputData.getSideOutput(summer);
        DataStream<Product> autumnStream = sideOutputData.getSideOutput(autumn);
        DataStream<Product> winterStream = sideOutputData.getSideOutput(winter);

        // 輸出標籤為:winter 的數據流
        winterStream.print();

        env.execute("output");
    }
}

結果輸出:

更多文章:www.ipooli.com

掃碼關注公眾號《ipoo》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

時間序列神器之爭:Prophet VS LSTM

一、需求背景

我們福祿網絡致力於為廣大用戶提供智能化充值服務,包括各類通信充值卡(比如移動、聯通、電信的話費及流量充值)、遊戲類充值卡(比如王者榮耀、吃雞類點券、AppleStore充值、Q幣、鬥魚幣等)、生活服務類(比如肯德基、小鹿茶等),網娛類(比如QQ各類鑽等),作為一個服務提供商,商品質量的穩定、持續及充值過程的便捷一直是我們在業內的口碑。
在整個商品流通過程中,如何做好庫存的管理,以充分提高庫存運轉周期和資金使用效率,一直是個難題。基於此,我們提出了智能化的庫存管理服務,根據訂單數據及商品數據,來預測不同商品隨着時間推移的日常消耗情況。

二、算法選擇

目前成熟的時間序列預測算法很多,但商業領域性能優越的卻不多,經過多種嘗試,給大家推薦2種時間序列算法:facebook開源的Prophet算法和LSTM深度學習算法。
現將個人理解的2種算法特性予以簡要說明:

  • (1)、在訓練時間上,prophet幾十秒就能出結果,而lstm往往需要1個半小時,更是隨着網絡層數和特徵數量的增加而增加。
  • (2)、Prophet是一個為商業預測而生的時間序列預測模型,因此在很多方便都有針對性的優化,而lstm的初衷是nlp。
  • (3)、Prophet無需特徵處理即可使用,參數調優也明確簡單。而lstm則需要先進行必要的特徵處理,其次要進行正確的網絡結構設計,因此lstm相對prophet更為複雜。
  • (4)、Lstm需要更多的數據進行學習,否則無法消除欠擬合的情形。而prophet不同,prophet基於統計學,有完整的數學理論支撐,因此更容易從少量的數據中完成學習。
  • (5)、傳統的時間序列預測算法只支持單緯度,但LSTM能支持多緯度,也就是說LSTM能考慮促銷活動,目標用戶特性,產品特性等

三、數據來源

  • (1)、訂單數據
  • (2)、產品分類數據

四、數據形式

time,product,cnt
2019-10-01 00,**充值,6
2019-10-01 00,***遊戲,368
2019-10-01 00,***,1
2019-10-01 00,***,11
2019-10-01 00,***遊戲,17
2019-10-01 00
,三網***,39
2019-10-01 00,**網,6
2019-10-01 00,***,2

字段說明:

  • Time:小時級時間
  • Product:產品名稱或產品的分類名稱,目前使用的是產品2級分類,名稱
  • Cnt:成功訂單數量
    目前的時間序列是由以上time和cnt組成,product是用於區分不同時間序列的字段。

五、特徵處理

時間序列一般不進行特徵處理,當然可以根據具體情況進行歸一化處理或是取對數處理等。

六、算法選擇

目前待選的算法主要有2種:

  • (1)、Prophet
    Facebook開源的時間序列預測算法,考慮了節假日因素。
  • (2)、LSTM
    優化后的RNN深度學習算法。

七、算法說明

7.1 prophet

7.1.1Prophet的核心是調參,步驟如下:
  • 1、首先我們去除數據中的異常點(outlier),直接賦值為none就可以,因為Prophet的設計中可以通過插值處理缺失值,但是對異常值比較敏感。
  • 2、選擇趨勢模型,默認使用分段線性的趨勢,但是如果認為模型的趨勢是按照log函數方式增長的,可設置growth=’logistic’從而使用分段log的增長方式
  • 3、 設置趨勢轉折點(changepoint),如果我們知道時間序列的趨勢會在某些位置發現轉變,可以進行人工設置,比如某一天有新產品上線會影響我們的走勢,我們可以將這個時刻設置為轉折點。如果自己不設置,算法會自己總結changepoint。
  • 4、 設置周期性,模型默認是帶有年和星期以及天的周期性,其他月、小時的周期性需要自己根據數據的特徵進行設置,或者設置將年和星期等周期關閉。
    設置節假日特徵,如果我們的數據存在節假日的突增或者突降,我們可以設置holiday參數來進行調節,可以設置不同的holiday,例如五一一種,國慶一種,影響大小不一樣,時間段也不一樣。
  • 5、 此時可以簡單的進行作圖觀察,然後可以根據經驗繼續調節上述模型參數,同時根據模型是否過擬合以及對什麼成分過擬合,我們可以對應調節seasonality_prior_scale、holidays_prior_scale、changepoint_prior_scale參數。

以上是理論上的調參步驟,但我們在實際情況下在建議使用grid_search(網格尋參)方式,直接簡單效果好。當機器性能不佳時網格調參配合理論調參方法可以加快調參速度。建議初學者使用手動調參方式以理解每個參數對模型效果的影響。

holiday.csv

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from fbprophet import Prophet

data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')


def get_product_data(name, rule=None):
    product = data[data['product'] == name][['cnt']]
    product.plot()
    
if rule is not None:
        product = product.resample(rule).sum()
    product.reset_index(inplace=True)
    product.columns = ['ds', 'y']
    return product


holidays = pd.read_csv('holiday.csv', parse_dates=['ds'])
holidays['lower_window'] = -1

holidays = holidays.append(pd.DataFrame({
    'holiday': '雙11',
    'ds': pd.to_datetime(['2019-11-11', '2020-11-11']),
    'lower_window': -1,
    'upper_window': 1,
})).append(pd.DataFrame({
    'holiday': '雙12',
    'ds': pd.to_datetime(['2019-12-12', '2020-12-12']),
    'lower_window': -1,
    'upper_window': 1,
})
)

def predict(name, rule='1d', freq='d', periods=1, show=False):
    ds = get_product_data(name, rule=rule)
    if ds.shape[0] < 7:
        return None
    m = Prophet(holidays=holidays)
    m.fit(ds)
    future = m.make_future_dataframe(freq=freq, periods=periods)  # 建立數據預測框架,數據粒度為天,預測步長為一年
    forecast = m.predict(future)
    if show:
        m.plot(forecast).show()  # 繪製預測效果圖
        m.plot_components(forecast).show()  # 繪製成分趨勢圖
    mse = forecast['yhat'].iloc[ds.shape[0]] - ds['y'].values
    mse = np.abs(mse) / (ds['y'].values + 1)
    return [name, mse.mean(), mse.max(), mse.min(), np.quantile(mse, 0.9), np.quantile(mse, 0.8), mse[-7:].mean(),
            ds['y'].iloc[-7:].mean()]
if __name__ == '__main__':
    products = set(data['product'])
    p = []
    for i in products:
        y = predict(i)
        if y is not None:
            p.append(y)
    df = pd.DataFrame(p, columns=['product', 'total_mean', 'total_max', 'total_min', '0.9', '0.8', '7_mean',
       '7_real_value_mean'])
    df.set_index('product', inplace=True)
    product_sum: pd.DataFrame = data.groupby('product').sum()
    df = df.join(product_sum)
    df.sort_values('cnt', ascending=False, inplace=True)
    df.to_csv('result.csv', index=False)

結果如下:由於行數較多這裏只展示前1行

根據結果,對比原生數據,可以得出如下結論:
就算法與產品的匹配性可分為3個類型:

  • (1)與算法較為匹配,算法的歷史誤差8分為數<=0.2的
  • (2)與算法不太匹配的,算法的歷史誤差8分為數>0.2的
  • (3)數據過少的,無法正常預測的。目前僅top10就能佔到整體訂單數的90%以上。
7.1.2 部分成果展示

A. 因素分解圖

上圖中主要分為3個部分,分別對應prophet 3大要素,趨勢、節假日或特殊日期、周期性(包括年周期、月周期、week周期、天周期以及用戶自定義的周期)
下面依照上面因素分解圖的順序依次對圖進行說明:

  • (1)、Trend:
    即趨勢因素圖。描述時間序列的趨勢。Prophet支持線性趨勢和logist趨勢。通過growth參數設置,當然模型能自己根據時間序列的走勢判斷growth類型。這也是prophet實現的比較智能的一點。
  • (2)、Holidays
    即節假日及特殊日期因素圖。描述了節假日及用戶自定義的特殊日期對時間序列的影響。正值為正影響,負值為負影響。從圖中可以看出這個商品對節假日比較敏感。節假日是根據holidays參數設置的。
  • (3)、weekly
    星期周期性因素圖。正常情況下,如果是小時級別數據將會有天周期圖。有1年以上完整數據並且時間序列有典型的年周期性會有年周期圖。如果你覺得這個有年周期,但模型並不這麼認為,你可以通過設置yearly_seasonality設置一個具體的數值。這個數值默認情況下為10(weekly_seasonality默認為3),這個值代表的是傅里恭弘=叶 恭弘級數的項數,越大模型越容易過擬合,過小則會導致欠擬合,一般配合seasonality_prior_scale使用。
    B.預測曲線與實際值對比

7.2 lstm

LSTM(長短記憶網絡)主要用於有先後順序的序列類型的數據的深度學習網絡。是RNN的優化版本。一般用於自然語言處理,也可用於時間序列的預測。

簡單來說就是,LSTM一共有三個門,輸入門,遺忘門,輸出門, i 、o、 f 分別為三個門的程度參數, g 與RNN中的概念一致。公式里可以看到LSTM的輸出有兩個,細胞狀態c 和隱狀態 h,c是經輸入、遺忘門的產物,也就是當前cell本身的內容,經過輸出門得到h,就是想輸出什麼內容給下一單元。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
from torch import nn

from sklearn.preprocessing import MinMaxScaler

ts_data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')


def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
    n_vars = 1 if type(data) is list else data.shape[1]
    df = pd.DataFrame(data)
    cols, names = list(), list()
    # input sequence (t-n, ... t-1)
    for i in range(n_in, 0, -1):
        cols.append(df.shift(i))
        names += [('var%d(t-%d)' % (j + 1, i)) for j in range(n_vars)]
    # forecast sequence (t, t+1, ... t+n)
    for i in range(0, n_out):
        cols.append(df.shift(-i))
        if i == 0:
            names += [('var%d(t)' % (j + 1)) for j in range(n_vars)]
        else:
            names += [('var%d(t+%d)' % (j + 1, i)) for j in range(n_vars)]
    # put it all together
    agg = pd.concat(cols, axis=1)
    agg.columns = names
    # drop rows with NaN values
    if dropnan:
        agg.dropna(inplace=True)
    return agg


def transform_data(feature_cnt=2):
    yd = ts_data[ts_data['product'] == '移動話費'][['cnt']]
    scaler = MinMaxScaler(feature_range=(0, 1))
    yd_scaled = scaler.fit_transform(yd.values)
    yd_renamed = series_to_supervised(yd_scaled
, n_in=feature_cnt).values.astype('float32')

    n_row = yd_renamed.shape[0]

    n_train = int(n_row * 0.7)

    train_X, train_y = yd_renamed[:n_train, :-1], yd_renamed[:n_train, -1]
    test_X, test_y = yd_renamed[n_train:, :-1], yd_renamed[n_train:, -1]

    # 最後,我們需要將數據改變一下形狀,因為 RNN 讀入的數據維度是 (seq, batch, feature),所以要重新改變一下數據的維度,這裏只有一個序列,所以 batch 是 1,而輸入的 feature 就是我們希望依據的幾天,這裏我們定的是兩個天,所以 feature 就是 2.
    train_X = train_X.reshape((-1, 1, feature_cnt))
    test_X = test_X.reshape((-1, 1, feature_cnt))
    print(train_X.shape, train_y.shape, test_X.shape, test_y.shape)

    # 轉化成torch 的張量
    train_x = torch.from_numpy(train_X)
    train_y = torch.from_numpy(train_y)
    test_x = torch.from_numpy(test_X)
    test_y = torch.from_numpy(test_y)
    return scaler, train_x, train_y, test_x, test_y


scaler, train_x, train_y, test_x, test_y = transform_data(24)


# lstm 網絡
class lstm_reg(nn.Module):  # 括號中的是python的類繼承語法,父類是nn.Module類 不是參數的意思
    def __init__(self, input_size, hidden_size, output_size=1, num_layers=2):  # 構造函數
        # inpu_size 是輸入的樣本的特徵維度, hidden_size 是LSTM層的神經元個數,
        # output_size是輸出的特徵維度
        super(lstm_reg, self).__init__()  # super用於多層繼承使用,必須要有的操作

        self.rnn = nn.LSTM(input_size, hidden_size, num_layers)  # 兩層LSTM網絡,
        self.reg = nn.Linear(hidden_size, output_size)  # 把上一層總共hidden_size個的神經元的輸出向量作為輸入向量,然後回歸到output_size維度的輸出向量中

    
def forward(self, x):  # x是輸入的數據
        x, _ = self.rnn(x)  # 單個下劃線表示不在意的變量,這裡是LSTM網絡輸出的兩個隱藏層狀態
        s, b, h = x.shape
        x = x.view(s * b, h)
        x = self.reg(x)
        x = x.view(s, b, -1)  # 使用-1表示第三個維度自動根據原來的shape 和已經定了的s,b來確定
        return x


def train(feature_cnt, hidden_size, round, save_path='model.pkl'):
    # 我使用了GPU加速,如果不用的話需要把.cuda()給註釋掉
    net = lstm_reg(feature_cnt, hidden_size)
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(net.parameters(), lr=1e-2)
    for e in range(round):
        # 新版本中可以不使用Variable了
        #     var_x = Variable(train_x).cuda()
        #     var_y = Variable(train_y).cuda()

        # 將tensor放在GPU上面進行運算
        var_x = train_x
        var_y = train_y

        out = net(var_x)
        loss = criterion(out, var_y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (e + 1) % 100 == 0:
            print('Epoch: {}, Loss:{:.5f}'.format(e + 1, loss.item()))
    # 存儲訓練好的模型參數
    torch.save(net.state_dict(), save_path)
    return net


if __name__ == '__main__':
    net = train(24, 8, 5000)
    # criterion = nn.MSELoss()
    # optimizer = torch.optim.Adam(net.parameters(), lr=1e-2)
    pred_test = net(test_x)  # 測試集的預測結果

    pred_test = pred_test.view(-1).data.numpy()  # 先轉移到cpu上才能轉換為numpy

    # 乘以原來歸一化的刻度放縮回到原來的值域
    origin_test_Y = scaler.inverse_transform(test_y.reshape((-1,1)))
    origin_pred_test = scaler.inverse_transform(pred_test.reshape((-1,1)))

    # 畫圖
    plt.plot(origin_pred_test, 'r', label='prediction')
    plt.plot(origin_test_Y, 'b', label='real')
    plt.legend(loc='best')
    plt.show()

    # 計算MSE
    # loss = criterion(out, var_y)?
    true_data = origin_test_Y
    true_data = np.array(true_data)
    true_data = np.squeeze(true_data)  # 從二維變成一維
    
MSE = true_data - origin_pred_test
    MSE = MSE * MSE
    MSE_loss = sum(MSE) / len(MSE)
    print(MSE_loss)

八、兩種算法的比較

  • (1)在訓練時間上,prophet幾十秒就能出結果,而lstm往往需要1個半小時,更是隨着網絡層數和特徵數量的增加而增加。
  • (2)Prophet是一個為商業預測而生的時間序列預測模型,因此在很多方便都有針對性的優化,而lstm的初衷是nlp。
  • (3)Prophet無需特徵處理即可使用,參數調優也明確簡單。而lstm則需要先進行必要的特徵處理,其次要進行正確的網絡結構設計,因此lstm相對prophet更為複雜。
  • (4)Lstm需要更多的數據進行學習,否則無法消除欠擬合的情形。而prophet不同,prophet基於統計學,有完整的數學理論支撐,因此更容易從少量的數據中完成學習。
    參考文獻:
    【1】Prophet官方文檔:https://facebook.github.io/prophet/
    【2】Prophet論文:https://peerj.com/preprints/3190/
    【3】Prophet-github:https://github.com/facebook/prophet
    【4】LSTM http://colah.github.io/posts/2015-08-Understanding-LSTMs/
    【5】基於LSTM的關聯時間序列預測方法研究 尹康 《北京交通大學》 2019年 cnki地址:http://cdmd.cnki.com.cn/Article/CDMD-10004-1019209125.htm

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新加坡銷毀重達9公噸走私象牙 打擊非法交易

摘錄自2020年8月11日中央社報導

新加坡今天(11日)銷毀重達9公噸的非法走私象牙,並透過網路直播,預計數天才能完成銷毀。當局表示,這是全球近年最大的銷毀非法象牙行動,展現星國打擊非法野生動物交易的決心。

根據動保人士估計,每天約有100隻非洲象被意圖盜取象牙等大象身體部位的盜獵者所殺,目前僅存約40萬隻非洲象。

新加坡是非洲與亞洲之間運送非法動物商品的海上航路點。除了對非法運輸的商品採取強硬立場,新加坡去年也宣示,自2021年9月起,將全面禁止國內象牙及其製品銷售。

生物多樣性
國際新聞
新加坡
象牙
野生動物
非洲象
非法盜獵
象牙走私

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

加拿大最後完整冰棚崩塌 面積縮 43% 、分裂出冰山面積比曼哈頓大

摘錄自2020年8月11日立場新聞報導

今年7月的最後兩日,加拿大北極群島中最北的埃爾斯米爾島(Ellesmere Island)米爾恩冰棚(Milne Ice Shelf)崩塌,使加拿大最後一個完整的冰棚面積減少了 43%,該冰塊其後漂入北冰洋,進一步分成兩大塊,並被歐洲太空總署哥白尼計劃哨兵衛星所拍攝到。

塌下的冰棚約有 80 平方公里,比 60 平方公里的曼哈頓更大。根據加拿大極冰局,高於正常的空氣溫度、離岸風和冰棚前的開揚水域,都是造成冰棚破裂的原因。由於冰棚破裂,北極最後一個已知的棚外湖 (epishelf lake) 可能消失。

冰棚可像活塞一樣,減緩與阻擋冰蓋與融冰水流入海洋的速度,有助於限制全球海水水位上升。另外,冰棚崩塌,造成的冰山可能會危害當地的航運業。

棚外湖是困於浮在海水上冰棚的淡水水體。當米爾恩冰棚崩塌時,可能將棚外湖以及其所含的淡水送進北冰洋,影響其鹹度,不過專家不確定其影響程度,情況將取決於米爾恩冰棚剩餘部分的完整性。

氣候變遷
國際新聞
加拿大
北極
冰棚
冰山融化

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

澳洲北領地邊界再封18個月 防原住民染疫

摘錄自2020年8月11日中央社報導

澳洲當局今天(11日)表示,將持續關閉北領地(Northern Territory)邊界18個月,以免其他疫情重災區的民眾進入,藉此保護當地龐大且弱勢的原住民族群。法新社報導,根據政府統計,北領地僅有約25萬人居住,其中30%為原住民。

外界認為澳洲原住民更容易受到武漢肺炎(COVID-19)等疾病的威脅,因為社會經濟與文化因素會影響醫療照護資源的取得及潛在健康問題。許多原住民族群擔憂疫情恐襲擊偏遠且醫療資源有限的原住民社區。

自澳洲爆發疫情以來,北領地確診病例很少,無人病歿。北領地近來不允許維多利亞州(Victoria)及雪梨的人進入當地。

土地利用
國際新聞
澳洲
原住民
疫情下的社會衝突
弱勢族群

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

印度空氣污染嚴重致或致八成蜜蜂死亡 糧食安全將受重大威脅

摘錄自2020年8月11日立場新聞報導

蜜蜂可能比人類更強烈地感受到空氣污染帶來的影響。最新刊於《美國國家科學院期刊》(PNAS) 的報告指,即使是輕微的空氣污染也會殺死 80% 的亞洲大蜜蜂(Apis Dorsata) 。學者表示這是首次提供全面證據,證明昆蟲對空氣中的懸浮粒子特別敏感。

印度多個城市都高居全球空氣污染最嚴重名單,不僅是 13.5 億人的家鄉,亦是亞洲大蜜蜂的棲息地。這種蜜蜂是南亞主要的花粉傳播媒介。芬蘭奧盧大學行為生態學家 Olli Loukola 指出,據估計如沒有昆蟲授粉, 53% 芒果會消失,單計印度的出口就因此損失約 8,600 萬美元。

研究發現,在污染更嚴重地點採集的蜜蜂更有可能出現含砷和鉛等有毒重金屬的顆粒。從污染最嚴重地區捕獲的 80% 蜜蜂在採樣後 1 天內死亡,是校園區蜜蜂死亡量的兩倍。團隊也發現,被有毒灰塵覆蓋的蜜蜂接觸花朵採蜜的次數僅是校園區蜜蜂的一半,因此可能減少有花植物成功授粉的機會。

生物多樣性
污染治理
國際新聞
印度
懸浮粒子
糧食安全
蜜蜂
授粉生物
空氣污染

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案