Scheme語言實例入門--怎樣寫一個「新型冠狀病毒感染風險檢測程序」

小學生都能用的編程語言

    2020的春季中小學受疫情影響,一直尚未開學,孩子宅在家說想作一個學校要求的研究項目,我就說你作一個怎麼樣經過編程來學習數學的小項目吧,用最簡單的計算機語言來解決小學數學問題。雖然我是一個老碼農,但一直不同意教小學生學編程,以爲這是揠苗滋長,小學生不該該過早的固化邏輯思惟而放鬆形象思惟,某些少兒編程機構竟然教學C++遊戲編程,我以爲這真是在摧殘祖國的花朵。如今孩子宅在家 ,想讓他學點什麼好幾回冒出學編程的想法都被本身給否決了,直到我看到數學老師要求同窗們整理小學階段的數學公式、概念,我看到有一個小朋友竟然畫出了平面幾何體的「繼承」關係,讓我眼前一亮:這種抽象關係若是用程序來表示不正合適嗎?明白抽象方法了,那麼學編程問題就不大了。因而我在想應該教孩子學什麼語言比較好:LOGO、VB仍是煊赫一時的Python?雖然我很是熟悉C#,但須要了解許多背景知識,還須要安裝一個很大的框架環境,顯然C#不適合小學生學習,Java也是。LOGO是老牌的兒童編程語言了,操控一個小海龜來畫圖很形象,VB入門簡單,但要一個小學生熟悉它的集成開發環境要求仍是高了點,選Python無非就是由於AI應用火它就火,除此以外我找不出它適合兒童使用的理由。python

我以爲給小學生使用的編程語言,要足夠簡單:git

1,編程環境足夠簡單,一個命令行就行,不須要一個強大的IDE,不然用它還得熟悉不少菜單按鈕和概念;github

2,語法要足夠簡單,最好連變量都不須要定義,沒有各類複雜的程序結構或語句,不須要了解方法、類、包、模塊這些東西,拿來就能寫程序跑起來;數據庫

3,數據結構要簡單,什麼數組、隊列、堆棧或者樹等等不要太多。編程

    想到這裏,惟一知足要求的就是Lisp語言了,它簡單到只有3種最基本的數據結構:原子、表和字符串;只有一種語法,就是符號表達式,數據和函數都是採用符號表達式定義的,這種符號表達式稱爲S一表達式,它是原予和表的總稱。Lisp衍生出了不少方言,造成一個龐大的Lisp語言家族,Scheme是其中最簡單的方言,並且很長時間都是美國麻省理工學院計算機系的教學語言,Scheme的發明者和推進者都是數學家、科學家和教育學家,因此它一開始就有數學的基因,很是適合做爲一種入門的計算機教學語言。前面說到孩子上數學課須要,那麼Scheme語言就是不二之選了,B站有一個小學生用Scheme語言來表達三角形定理應用的小視頻,能夠點擊這裏查看小程序

項目程序簡介

    既然決定教孩子Scheme語言,那我就得先熟悉一下它了,以前斷斷續續學習了幾回,一直沒有真正用過它因此始終沒學好,如今疫情期間,正好能夠用它來寫一個程序練手,而且使用這個有實際意義的程序做爲小學生學習Scheme語言的入門教程,因而有了本篇文章標題說的這個項目,一個2019新型冠狀病毒肺炎(COVID-19)感染風險自助檢測程序。爲了更好的介紹Scheme語言,本文將結合這個實例程序來介紹Scheme語言的語法元素,因而有了這篇博客文章。讀者能夠先從下面的倉庫地址克隆一份,包括源碼和Scheme運行程序。數組

源碼倉庫:https://github.com/bluedoctor/Check-COVID-19網絡

程序名稱:2019新型冠狀病毒肺炎(COVID-19)感染風險自助檢測程序數據結構

程序功能:app

根據網絡上收集整理的新冠肺炎臨牀症狀表現,以及國家衛生健康委員會與國家中醫藥管理局發佈了《新型冠狀病毒肺炎診療方案》等資料, 整理的新冠肺炎臨牀症狀表現、醫院檢查和流行病學調查狀況,設計的一個風險測試表,而後根據這個風險測試表定義的診斷知識編寫程序,交互式的引導用戶回答提問,最後給出診斷結果。 你也能夠調整這裏定義的各個指標風險值,以使它更接近實際的效果。

 有了這個風險測試表,如何簡單有效的用程序表示,這也是我選擇使用Scheme語言來寫這個程序的緣由,由於它的S表達式具備程序和數據的一致性,也就是說咱們的知識數據能夠表達爲一種等價的程序結構,好比將上表的身體症狀表達爲下面的程序結構:

; 2019新型冠狀病毒肺炎(COVID-19)感染風險自助檢測程序
(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))
(define A2 (list "咳嗽" (cons "無痰" 15) (cons "有痰難吐" 10) (cons "有痰易吐" -10)))
(define A3 (list "乏力" (cons "" -15) (cons "輕微" 15) (cons "明顯" 30)))
(define A4 (list "腹瀉" (cons "" 0) (cons "輕微" 10) (cons "明顯" 5)))
(define A5 (list "呼吸困難" (cons "" 0) (cons "略感胸悶" 15) (cons "明顯" 30)))

這個症狀風險知識的程序表示代碼涉及的Scheme語言概念下面逐一介紹。

Scheme語言基礎

1,表達式

最簡單的表達式是常量對象,如字符串、數字、符號和列表。表達式支持其它對象類型,但這四種對象對大多數程序已經足夠了。

數字型(number)

它又分爲四種子類型:整型(integer),有理數型(rational),實型(real),複數型(complex);它們又被統一稱爲數字類型(number)。
如:複數型(complex) 能夠定義爲 (define c 3+2i)
實數型(real)能夠定義爲 (define f 22/7)
有理數型(rational)能夠定義爲 (define p 3.1415)
整數型(integer) 能夠定義爲 (define i 123)

符號類型(symbol)

是Scheme語言中有多種用途的符號名稱,它能夠是單詞,用括號括起來的多個單詞,也能夠是無心義的字母組合或符號組合,它在某種意義上能夠理解爲C中的枚舉類型。可使用quote操做符定義一個符號,也能夠單引號'開頭來簡單表示一個符號,以下面的示例:

> (quote a)
a
>'a
a

在Lisp/Scheme 中,一般都須要對錶達式進行求值,而符號(一般)不對自身求值,因此要是想引用符號,應該像上例那樣用 ' 引用它。

複合表達式

由操做符、常量對象或者表達式組合而成。例以下面這個計算兩個數相加的簡單表達式:

> (+ 1 2)
3

經過這個程序示例看到,Scheme的表達式是前綴表達式,也就是說把運算符放在最左側。這樣作的優勢是能夠定義帶任意個數的實參過程。

上面這個複合表達式若是想引用它而不是當即求值,就須要把它定義成符號:

> '(+ 1 2)
(+ 1 2)

這樣,咱們能夠在後續須要的時候將這個符號轉換成普通的表達式讓它求值。

S-表達式

Lisp 要求咱們直接在抽象語法上工做。這個抽象的語法樹用成對的括號表示。這樣的結構在 Lisp 圈子裏面被稱爲 sexp 表達式,俗稱S-表達式。S-表達式的第一個單詞決定了 sexp 的意義,剩下的單詞都是參數,記住這一條規則足夠了。這個規則使得咱們不須要記憶其它編程語言那些複雜的語法結構,入門使用變得極其簡單。咱們在設計程序的時候應該始終圍繞這個抽象語法進行,咱們的程序設計的越抽象,那麼程序就越接近問題的本質。

Lisp 程序當作是徹底由"函數調用"這個單一的語法結構構成。 Lisp 裏面沒有爲了算術表達式、或者邏輯表達式、或者語言的關鍵字,好比 IF 和 THEN,來準備特別的語法結構。全部的語言元素在 Lisp 裏面都是按照這個簡單一致的語法結構來安排,整個程序就是一個表達式,程序的運行就是對錶達式進行求值。

2,原子

 Lisp中有一個叫原子的東西,不可再分,是一個很基礎的概念。原子能夠是任何數,分數,小數,天然數,負數等等。原子能夠是一個字母排列,固然其中能夠夾雜數字和符號。除了表和全部函數之外均是原子。

Scheme沒有直接說原子這個概念,但Scheme做爲Lisp的方言,在形式上仍是有原子這樣的東西。全部的 Lisp/Scheme 表達式,要麼是 1 這樣的數原子,要麼是包在括號裏,由零個或多個表達式所構成的列表。因此能夠這樣說,List程序裏面就是原子和表。這就是Lisp 表示法一個美麗的地方是:它是如此的簡單!

3,表(list)

 表是由多個相同或不一樣的數據連續組成的數據類型,它是編程中最經常使用的複合數據類型之一,不少過程操做都與它相關。下面是在Scheme中表的定義和相關操做:

> (define la (list 1 2 3 4 ))
>la
(1 2 3 4)
> (length la) ; 取得列表的長度
4
> (list-ref la 3) ; 取得列表第3項的值(從0開始)
4
> (list-set! la 2 99) ; 設定列表第2項的值爲99
99
> la
(1 2 99 4)
> (define y (make-list 5 6)) ;建立列表
> y
(6 6 6 6 6)

在上面的例子中,使用了函數list 來構造具備4個元素的表,而後使用define函數來定義一個變量 la,將變量la與前面定義的表相綁定。除了使用list函數形勢來構造表,還可使用引用方式來使用一個表,下面的代碼與上面定義la 變量綁定表是等價的:

> (define la (list 1 2 3 4 ))
>la
(1 2 3 4)
> (define la '(1 2 3 4 ))
>la
(1 2 3 4)

回到文章開頭,我在這個小項目中首先就定義了幾個表而且與變量相綁定:

(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))

 

4,點對(pair)

List/Scheme 的基本構成元素是所謂的 Pair。什麼是 Pair 呢??咱們能夠按照這個英語單詞在平常生活中最多見的意思,好比一個座標點,來想象這個最基礎的數據結構。好比座標(1,2)是一個點對。一個點對包含兩個指針,每一個指針指向一個值。咱們用函數cons構造點對。好比說(cons 1 2)就構造出點對(1 . 2)。由於點對老是由函數cons構造,點對又叫作cons cell。點對左邊的值能夠用函數car取出來,右邊的值能夠由函數cdr取出來。好比列表(1 2 3 4),實際上由點對構成:(1 . (2 . (3 . 4. ‘())。能夠看出,列表本質是單向鏈表。

在當前實例程序的表變量A1中,咱們構造了一個具備發熱症狀對應風險屬性的表,它有三個點對元素,分別是:

( 三天內 . 5  ) ( 三天到一週 . 10  )  (超過一週 . 15)

構造這3個點對元素分別經過下面的三個表達式實現:

(cons "三天內" 5)(cons "三天到一週" 10)(cons "超過一週" 15)

在表變量A1 中,能夠經過cdr函數獲得這3個點對元素:

>(car A1)

發熱

>(cdr A1)

( 三天內 . 5  ) ( 三天到一週 . 10  )  (超過一週 . 15)

能夠看出,咱們在程序中,使用點對模擬了症狀屬性和對應的風險值結構,相似於.NET中的「名-值」對結構。因此,咱們經過Scheme程序,實現了新冠病毒臨牀診斷知識表達的 「症狀--屬性--風險值」 三元結構,咱們的程序就是匹配患者的這些症狀屬性,從而計算出相對應的風險值。Scheme的表和點對結構,使得咱們對於這類知識的表達更直觀更容易。

5,向量(vector)

向量能夠說是一個很是好用的類型 ,是一種元素按整數來索引的對象,異源的數據結構,在佔用空間上比一樣元素的列表要少,在外觀上:
列表示爲: (1 2 3 4)
VECTOR表示爲: #(1 2 3 4)

能夠經過下面的代碼來定義和查看向量的表示:

>(define v (vector 1 2 3 4 5))
#(1 2 3 4 5)
>(define v ‘#(1 2 3 4 5))
#(1 2 3 4 5)
> (vector-ref v 0) ; 求第n個變量的值
1
> (vector-length v) ; 求vector的長度
5
> (vector-set! v 2 "abc") ; 設定vector第n個元素的值
> v
#(1 2 "abc" 4 5)
> (define x (make-vector 5 6)) ; 建立向量表
> x
#(6 6 6 6 6)

在當前項目實例中,咱們將全部相關的症狀變量放到一個向量中:

(define QA (vector A1 A2 A3 A4 A5))

以後,咱們會在程序中循環遍歷這些症狀表,獲取向量QA的長度,得到QA指定索引的表元素。

 

6,變量

變量定義:
能夠用define來定義一個變量,形式以下:
(define 變量名 值)
例如,上面定義了一個變量QA,它的值是一個向量。

前面的示例中好多地方都採用了這種方式來定義變量,但這種方式定義的是一個全局變量,但不少時候,咱們須要使用局部變量,以消除全局變量可能意外被修改的影響。

定義局部變量須要使用let表達式,以下所示:

> (let ((x 1) (y 2))
     (+ x y))
3

這裏定義了兩個變量:x和y,定義的時候給它們分別綁定一個初始值1和2。

let表達式的形式定義是:

(let [binds]

  bodys

)

變量在binds定義的形式中被聲明並初始化。body由任意多個S-表達式構成。binds的格式以下:
[binds] → ((p1 v1) (p2 v2) ...)
聲明瞭變量p一、p2,並分別爲它們賦初值v一、v2。變量的做用域(Scope)爲body體,也就是說變量只在body中有效。body體的最後一個表達式的值爲let表達式的值。

更改變量的值:
能夠用set!來改變變量的值,格式以下:
(set! 變量名 值)

如在當前實例程序中:

(define Total_Risk 0) ;定義全局 風險總值 變量
;其它代碼略
 (display "單純性發熱,調低風險值。")(newline)
 (set! Total_Risk (- Total_Risk 15))

Scheme語言是一種高級語言,和不少高級語言(如python,perl)同樣,它的變量類型不是固定的,能夠隨時改變。

7,函數

Scheme語言中函數不一樣於其它語言的函數,它被稱爲過程(Procedure)。過程是一種數據類型,這也是爲何Scheme語言將程序和數據做爲同一對象處理的緣由。

若是咱們在Chez Scheme提示符下輸入加號而後回車,會出現下面的狀況:

> +
#<procedure +>

這告訴咱們"+"是一個過程,並且是一個原始的過程,即Scheme語言中最基礎的過程,在GUILE中內部已經實現的過程,這和類型判斷同樣,如boolean?等,它們都是Scheme語言中最基本的定義。

define不只能夠定義變量,還能夠定義過程,因在Scheme語言中過程(或函數)都是一種數據類型,因此均可以經過define來定義。不一樣的是標準的過程定義要使用lambda這一關鍵字來標識。
咱們能夠自定義一個簡單的過程並使用,以下:

> (define add1 (lambda (x) (+ x 1)))
> add1
#<procedure add1>
> (add1 5)
6

在Scheme語言中,也能夠不用lambda,而直接用define來定義過程,它的格式爲:
(define (過程名 參數) (過程內容 …))

在當前實例程序中,都使用了這種方式來定義過程,我的以爲這種方式更方便些。例以下面定義一個過程確保用戶輸入一個數字並返回:

(define (input_selected )
    (let loop ()
        (display "請輸入你選擇的答案對應的數字:")  
        (let ((k (read)))   
            (if (integer? k)    
                k ;return
                (begin
                     (display "輸入錯誤!")  
                     (newline)
                     (loop))))))

在Scheme語言中,過程定義也能夠嵌套,通常狀況下,過程的內部過程定義只有在過程內部纔有效,至關C語言中的局部變量。

8,類型判斷

Scheme語言中全部判斷都是用類型名加問號再加相應的常量或變量構成,例如上面定義的函數input_selected 中使用 integer? 來判斷用戶輸入的內容是不是一個整數:(integer? k)

也可使用null?判斷一個對象是不是空類型:

(null? (cdr name-values))

 

9,邏輯運算

邏輯運算其實是計算一個邏輯表達式。原子邏輯表達式是布爾對象,在Scheme中使用 #t 表示true,#f 表示false。

邏輯表達式支持not,and,or 邏輯操做,類型判斷的結果也是一個邏輯表達式,例如在本項目實例中:

 ;測試是否有嚴重腹瀉而且沒有咳嗽
  (let ((FU_XIE (have_attribute_inResult Ctx_Attributes "腹瀉")))
      (if (and    (not (null? FU_XIE)) 
                  (equal? "明顯" (car (cdr FU_XIE))) 
                  (null? KE_SOU)
          )
          (set! Total_Risk 0) ;爲嚴重腹瀉引發的乏力,不會是新冠感染。
          (begin
              (display "乏力伴隨咳嗽或者發熱,調高風險值。")(newline)
              (set! Total_Risk (* Total_Risk 2))
          )
      )
  )

 and和or表達式均可以處理多個邏輯表達式參數,例如上面的and 表達式處理了三個邏輯判斷表達式。

10,代碼塊(form)

 塊(form)是Scheme語言中的最小程序單元,一個Scheme語言程序是由一個或多個form構成。沒有特殊說明的狀況下 form 都由小括號括起來,因此一個form最小能夠是一個表達式,也能夠是一個變量定義,一個過程定義。form容許嵌套,這使它能夠輕鬆的實現複雜的表達式,同時也是一種很是有本身特點的表達式。有時候須要將多個表達式組合成一個form,好比在分支判斷後執行多個操做,須要在一個塊中,這個功能能夠經過begin來實現,例如上面邏輯運算的例子。

11,分支結構

if結構:

Scheme語言的if結構有兩種格式:
(if (測試條件)(知足測試條件時 要執行的過程1))
(if (測試條件)(知足測試條件時 要執行的過程1)
                       (不然 要執行的過程2) )

要執行的過程1和2能夠是一個表達式,也能夠是多個表達式,若是是多個表達式,須要使用begin語句塊。if結構的示例代碼比較多,這裏就再也不重複了。

cond結構:

Scheme語言中的cond結構相似於C語言中的switch結構,cond的格式爲:

(cond ((測試) 操做) … (else 操做))

cond結構的每一個測試均可以是不一樣的邏輯表達式,至關於多個嵌套的if...else if...結構,在當前實例項目中就曾經使用cond結構來優化,示例代碼以下:

;從結果中判斷是否有指定的症狀屬性;若是有,返回症狀特徵表;若是沒有,返回空表
(define (have_attribute_inResult result attName)
    (let loop ( (lst result) )
        (let ( (car_lst (car lst))  (cdr_lst (cdr lst)))
            ;(if (equal? attName (car car_lst))
            ;    car_lst
            ;     (if (null? cdr_lst)
            ;        '()
            ;        (loop cdr_lst)
            ;    )
            ;)
            ;
            (cond   ((equal? attName (car car_lst))  car_lst )
                    ((null? cdr_lst) '())
                    (else (loop cdr_lst))
            )
        )
    )
)

case結構:

case結構和cond結構有點相似,它的格式爲:

(case (表達式) ((值) 操做)) ... (else 操做)))

case結構中的值能夠是複合類型數據,如列表,向量表等,只要列表中含有表達式的這個結果,則進行相應的操做,以下面的代碼:

 >(case (* 2 3)
   ((2 3 5 7) 'prime)
   ((1 4 6 8 9) 'composite))
composite

上面的例子返回結果是composite,由於列表(1 4 6 8 9)中含有表達式(* 2 3)的結果6;

在當前實例項目中,使用case結構來進行流行病調查分析:

(display "2)最近14天,您是否去過 重點疫區國家?(0-未去過,1-意大利、西班牙,2-歐洲其它地方,3-美國,4-世界其它地方)")
(newline)
(set! Total_Risk (+ Total_Risk 
    (case (input_selected)
        ((0) 0)
        ((1) 50)
        ((2 4) 30)
        ((3) 80)
        (else 0)
    )
))

12,循環結構

使用遞歸實現循環:

Scheme最原始的語法結構是沒有循環的,要實現循環功能,能夠經過遞歸調用過程實現:

>(define loop
       (lambda(x y)
           (if (<= x y)
               (begin
                  (display x) (display #\\space)
                 (set! x (+ x 1))
       (loop x y)))))
>(loop 1 10)
1 2 3 4 5 6 7 8 9 10

使用遞歸過程來實現循環雖然比較直觀天然,然而它的效率不高。要改善遞歸效率,能夠將遞歸過程修改成尾遞歸,Scheme語言規範規定尾遞歸將在語言內部優化爲循環結構。有關尾遞歸的概念這裏再也不詳細介紹。

使用命名let:

let表達式本質上是一個Scheme語法糖,它內部轉換成了lambda表達式調用。命名let在Scheme中與尾遞歸有類似的效果,具體能夠參考這篇文章

在本實例項目中,使用了命名let來實現循環,例如前面示例的函數have_attribute_inResult的定義裏面使用了命名let,下面看一個簡單的例子,從新看到input_select函數的示例,注意let後的符號 loop就是它的名字:

(define (input_selected )
    (let loop ()
        (display "請輸入你選擇的答案對應的數字:")  
        (let ((k (read)))   
            (if (integer? k)    
                k ;return
                (begin
                     (display "輸入錯誤!")  
                     (newline)
                     (loop))))))

在上面的過程當中,若是輸入的不是整數,會要求從新輸入,循環執行這個過程直到輸入正確爲止,在最後一行經過從新調用前面名字爲 loop的let實現循環效果。而在函數have_attribute_inResult中,命名let開始的時候將變量lst的初始值綁定爲函數參數result,而在方法後面部分調用名字loop的let時候,使用變量cdr_lst來更新最這個命名let的值:

(define (have_attribute_inResult result attName)
    (let loop ( (lst result) )
        (let ( (car_lst (car lst))  (cdr_lst (cdr lst)))
             (cond   ((equal? attName (car car_lst))  car_lst )
                    ((null? cdr_lst) '())
                    (else (loop cdr_lst))
            )
        )
    )
)

使用do循環表達式:

就像在C系列語言中咱們一般用while比較多而do比較少同樣,在scheme中do也並不常見,但語法do也能夠用於表達重複。它的格式以下:

    (do binds
       (predicate value)
        body)

變量在binds部分被綁定,而若是predicate被求值爲真,則函數從循環中逃逸(escape)出來,並返回值value,不然循環繼續進行。binds部分的格式以下所示:
[binds] - ((p1 i1 u1) (p2 i2u2)...)
變量p1,p2,…被分別初始化爲i1,i2,…並在循環後分別被更新爲u1,u2,…。

在當前實例項目中,也使用了do循環表達式,以下所示:

 1 ;獲取症狀特徵列表指定的特徵序號(從1開始的序號)對應的風險值
 2 (define (get-index-value lst_attribute index)
 3     (do ((name-values (cdr lst_attribute) (cdr name-values)) 
 4          (i 1 (+ i 1)))
 5         ( (or (= i index) (null? (cdr name-values))) ;break
 6           (cdr (car name-values)) ;return value
 7         )
 8         ;(display (cdr name-values ))   
 9         ;(newline) 
10     )
11 )

在上面的第3行代碼中,循環變量name-values綁定的初始值是表達式(cdr lst_attribute),而在後續的循環過程當中,循環變量name-values將被更新爲一個新的值:表達式(cdr name-values)的值,也就是後續的症狀(屬性.風險值)點對。第5行是跳出循環的條件表達式,第6行代碼是do循環表達式在終止循環後返回的值。do循環的body代碼部分能夠沒有,上面的示例中將body代碼註釋了。

系統設計方案

1,診斷方案

根據《新型冠狀病毒肺炎診療方案》,這裏整理一個簡單的診斷方案。這裏須要從三方面進行。

首先是對患者身體症狀的問診,詢問是否有發熱、咳嗽、腹瀉、乏力、呼吸困難等新冠肺炎典型的症狀,再根據具體的症狀表現來分析感染病毒的風險概率,通過綜合分析,設計出每個具體症狀表現的風險值。

而後是醫院化驗檢測報告分析,主要有胸部CT、核酸檢測以及全血化驗中的白細胞計數狀況,若是核酸檢測陽性那感染新冠病毒的概率就是100%,但檢測陰性並不表明沒有感染概率,核酸檢測有一個準確性問題以及隨着病情發展的檢測時間問題,都會影響檢測結果準確性。因此後來對不少疑似患者採起了依據肺部CT結果的臨牀確診。最後就是白細胞計數,病毒感染通常白細胞計數都不會有明顯變化,甚至降低,若是白細胞計數顯著增高,那麼要排除病毒感染,但也多是合併細菌性炎診感染。多以對於醫院化驗檢測狀況也須要綜合設定一個風險值。

第三就是流行病學調查,對於有疑似感染的狀況,若是與確診患者有接觸,或者去太重點疫區,那麼感染的風險就很高,反之就較低。根據流調的具體狀況,對每一類狀況設定一個相應的風險值。

綜合這三方面的分析,制定一個風險測試表。不過這個表也僅僅只能作一個參考,一方面具體的風險值須要專家來綜合設定,另外一方面這些風險因素是相互影響的,它們的關係不是一個表格這麼簡單,因此須要把這個風險測試表再根據患者有的測試項目進行綜合分析,根據一些診斷規則,對風險值進行修正。因此這個診斷方案,很是依賴行業專家的知識,包括科學家、醫學家以及臨牀一線醫生。鑑於筆者能力水平,當前這個診斷方案確定會有不少問題,僅供參考和學習Scheme語言編程使用。

2,專家系統

專家系統是一個具備大量的專門知識與經驗的程序系統,是一種模擬人類專家解決領域問題的計算機程序系統。顯然,寫一個診斷新冠病毒感染風險的程序能夠是一個專家系統,前面也說了這個診斷方案是依賴於專家的知識經驗的。
專家系統一般由人機交互界面、知識庫、推理機、解釋器、綜合數據庫、知識獲取等6個部分構成。其中尤以知識庫與推理機相互分離而別具特點。下圖是專家系統的一個結構示意圖:

當前這個「診斷專家系統」程序中,也具備專家系統基本的元素,它們的設計方案是:

  • 人機界面:採用交互式命令行方式,系統提問,用戶根據提是選擇回答問題,引導用戶完成全部問題的測試。
  • 知識庫:將「新冠病毒感染風險測試表」的知識使用Scheme程序表示。
  • 全局數據庫:一個存儲當前患者對象診斷數據的「特徵上下文」對象,它是一個列表。
  • 解釋機制:每當用戶回答一個問題,都會給出當前特徵項的風險值,並在診斷完成後給出診斷數據。
  • 推理機:它包括2個工做,一是根據用戶回答的問題計算感染風險的累加值,另外一個是根據一些特殊的風險屬性相關性規則,來修正風險值,好比對於乏力症狀特徵與其它症狀特徵之間的關係進行處理的規則。

3,推理機制

採用不肯定性推理機制。不肯定性推理是指那種創建在不肯定性知識和證據的基礎上的推理。它其實是一種從不肯定的初始證據出發,經過運用不肯定性知識,最終推出既保持必定程度的不肯定性,又是合理和基本合理的結論的推理過程,目的是使計算機對人類思惟的模擬更接近於人類的真實思惟過程。

在當前項目中,患者的症狀表現就是疾病診斷的一種證據,但這種證據所表明的診斷知識是不肯定的。好比發熱症狀,不少疾病都會引發發熱,通常感冒和流感都會發熱,但發熱的症狀表現(例如持續時間和體溫)是不一樣的,同一個症狀表現也多是不一樣疾病引發的,因此針對某一種疾病而言,每一個症狀表現都只能是一個該種疾病的確診機率,須要綜合多個症狀表現才能在一個大機率上確認或者排除一種疾病。好比發熱三天之內,沒有咳嗽和呼吸困難,也沒有乏力表現,那麼是新冠的可能性就很低,若是還有鼻塞流鼻涕的表現,那麼普通感冒的可能性就很大了。

在當前項目中,不肯定性知識的表達採用【對象】-【特徵】-【值】結構,【值】是一個「點對」結構,這個「點對」結構表示【值】的名稱和【值】的數值結果,在當前程序中,【值】的具體數值表示感染風險值。例如發熱是患者對象的一個症狀特徵,發熱的具體表現,也就是特徵的值,例如發熱」三天內「是發熱症狀的特徵值,它對應的新冠感染風險是5%。在當前項目中,發熱症狀的完整特徵值表達爲Scheme語言的「列表」,以下所示:

(list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15))

爲了便於引用這個發熱症狀的不肯定性知識,咱們將它綁定到一個變量上,至關於爲它定義一個編號:

(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))

這樣,將咳嗽、乏力、腹瀉、呼吸困難等症狀都定義一個編號,組成一個身體症狀「矩陣」,這個矩陣結構用一個向量來表示:

;定義 患者的身體症狀特徵
(define A1 (list "發熱" (cons "三天內" 5) (cons "三天到一週" 10) (cons "超過一週" 15)))
(define A2 (list "咳嗽" (cons "無痰" 15) (cons "有痰難吐" 10) (cons "有痰易吐" -10)))
(define A3 (list "乏力" (cons "" -15) (cons "輕微" 15) (cons "明顯" 30)))
(define A4 (list "腹瀉" (cons "" 0) (cons "輕微" 10) (cons "明顯" 5)))
(define A5 (list "呼吸困難" (cons "" 0) (cons "略感胸悶" 15) (cons "明顯" 30)))
(define QA (vector A1 A2 A3 A4 A5))

使用【對象】-【特徵】-【值】結構,咱們處理能夠定義患者的身體症狀特徵,還能夠定義患者的醫院化驗檢查結果特徵:

;定義患者的醫院化驗檢查結果特徵
(define B1 (list "胸部CT" (cons "正常/未檢測" 1) (cons "肺部毛玻璃樣" 80) (cons "其它狀況" 20)))
(define B2 (list "病毒核酸檢測" (cons "未檢測" 5) (cons "陽性" 100) (cons "陰性" 20)))
(define B3 (list "白細胞計數" (cons "正常/未檢測" 1) (cons "偏低" 20) (cons "增高" -20)))
(define QB (vector B1 B2 B3 ))

如今能夠看到,經過這個【對象】-【特徵】-【值】結構,咱們能夠表示通常的「不肯定性知識」了,甚至用來表示學生考試題的答題選擇率,用來發現班級的教學問題。

上面的每一個特徵都將造成一個要向患者詢問的問題,這些問題的回答結果將在患者對象身上造成一個特徵上下文。特徵上下文的結構是一個表,包含多個具體的特徵表,例如:
( (特徵名1 特徵值名 特質值) (特徵名2 特徵值名 特質值) ...)

在當前程序中,它的值可能像下面這個樣子:

 ((發熱 三天內 5) (咳嗽 無痰 15) (乏力 無 -15) (腹瀉 無 0) (呼吸困難 無 0))

患者對象的特徵上下文對象,在程序中經過下面一行代碼實現:

(define Ctx_Attributes '())

4,推理機的實現

有了推理所須要的不肯定性知識表達結構,那麼進行推理就很方便了:特徵匹配。推理過程就是在與用戶的交互過程當中,經過詢問用戶的問題,若是該問題與預先定義的不肯定性特徵知識相匹配,那麼就能夠計算它對應的機率值(在本項目中是風險值)。推理的結果存放到特徵上下文對象中,在本程序中,它是患者對象的症狀問題上下文。

在本程序中,推理機的實現就是過程process-question 的定義,它會遍歷特徵向量中的每個特徵表,計算出匹配的特徵值。過程實現的詳細代碼以下:

(define (process-question listAttributes)
    (let loop ((i 0) (j (- (vector-length listAttributes) 1)))
    (display (+ i 1))
    (display ",您最近是否有【")
    (let ((Ai (vector-ref listAttributes i)))
        (display (car Ai))
        (display "】的狀況?(若是有,請輸入數字1;不然輸入其它字符以跳過此項檢測。)")
        (newline)
        ;(let ((input 1)) ;test
        (let ((input (read)) (Ai_Name (car Ai)))
            (if (and (integer? input) (= input 1))
                (begin
                    (display Ai_Name)
                    (display "的具體狀況是:")
                    (show-attribute Ai)
                    (newline)
                    (let ((q_index (input_selected )))
                        (display "您當前選擇的狀況風險值是:")
                        (let ((curr_risk (get-index-value Ai q_index))) 
                            (display curr_risk)
                            (set! Ctx_Attributes (append Ctx_Attributes 
                                (list (list Ai_Name (get-index-name Ai q_index) curr_risk ))))
                            (set! Total_Risk (+ Total_Risk curr_risk)) 
                        )
                        
                    ) 
                )
                (begin
                    (display "您沒有【")
                    (display Ai_Name)
                    (display "】的狀況。")
                )
            )
        )
        (newline)
        (newline)
    )

    (if (< i j)
        (loop (+ i 1) j))
))

下面咱們就能夠調用這個「推理機」來處理問題了:

(display " 1、開始身體症狀測試 :")(newline)
(process-question QA)
(display "初步診斷詳細內容:")
(display Ctx_Attributes )
(newline)  
;其它代碼略
(display " 2、開始進行【醫院檢測結果】分析 :")(newline)
(process-question QB)

 

結語

當前項目實例程序的完整代碼請去前面說的Git倉庫克隆一份,整個代碼不到300行,其中不少代碼已經做爲Scheme語言基礎的示例來介紹了,因此這裏再也不重複。經過這個項目,咱們看到用Scheme語言來實現這樣一個新冠病毒感染風險測試的專家系統是比較簡單的,相信你閱讀了本篇問診而且下載運行這個程序以後,已經踏入了Scheme語言學習的大門。

相關文章
相關標籤/搜索