基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)

上篇文章完成了文章增刪改的接口和友情鏈接列表的接口,本篇繼續。

善於思考的同學肯定發現,在執行增刪改操作后,Redis緩存中的數據還是存在的,也就意味着查詢接口返回的數據還是舊的,所以在寫接口之前,先完成一下清緩存的操作。

移除緩存

移除緩存我這裏找了一個新的包:Caching.CSRedis,選他是因為微軟的包Microsoft.Extensions.Caching.StackExchangeRedis沒有給我們實現批量刪除的功能。

Caching.CSRedis開源地址,https://github.com/2881099/csredis 在這不做過多介紹,感興趣的自己去看。

.Application.Caching層添加包Caching.CSRedisInstall-Package Caching.CSRedis,然後在模塊類MeowvBlogApplicationCachingModule中進行配置。

//MeowvBlogApplicationCachingModule.cs
...
public override void ConfigureServices(ServiceConfigurationContext context)
{
    ...

    var csredis = new CSRedis.CSRedisClient(AppSettings.Caching.RedisConnectionString);
    RedisHelper.Initialization(csredis);

    context.Services.AddSingleton<IDistributedCache>(new CSRedisCache(RedisHelper.Instance));
}
...

直接新建一個移除緩存的接口:ICacheRemoveService,添加移除緩存的方法RemoveAsync()。代碼較少,可以直接寫在緩存基類CachingServiceBase中。

public interface ICacheRemoveService
{
    /// <summary>
    /// 移除緩存
    /// </summary>
    /// <param name="key"></param>
    /// <param name="cursor"></param>
    /// <returns></returns>
    Task RemoveAsync(string key, int cursor = 0);
}

然後可以在基類中實現這個接口。

public async Task RemoveAsync(string key, int cursor = 0)
{
    var scan = await RedisHelper.ScanAsync(cursor);
    var keys = scan.Items;

    if (keys.Any() && key.IsNotNullOrEmpty())
    {
        keys = keys.Where(x => x.StartsWith(key)).ToArray();

        await RedisHelper.DelAsync(keys);
    }
}

簡單說一下這個操作過程,使用ScanAsync()獲取到所有的Redis key值,返回的是一個string數組,然後根據參數找到符合此前綴的所有key,最後調用DelAsync(keys)刪除緩存。

在需要有移除緩存功能的接口上繼承ICacheRemoveService,這裏就是IBlogCacheService

//IBlogCacheService.cs
namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService : ICacheRemoveService
    {
    }
}

在基類中已經實現了這個接口,所以現在所有繼承基類的緩存實現類都可以調用移除緩存方法了。

MeowvBlogConsts中添加緩存前綴的常量。

//MeowvBlogConsts.cs
/// <summary>
/// 緩存前綴
/// </summary>
public static class CachePrefix
{
    public const string Authorize = "Authorize";

    public const string Blog = "Blog";

    public const string Blog_Post = Blog + ":Post";

    public const string Blog_Tag = Blog + ":Tag";

    public const string Blog_Category = Blog + ":Category";

    public const string Blog_FriendLink = Blog + ":FriendLink";
}

然後在BlogService.Admin.cs服務執行增刪改后調用移除緩存的方法。

//BlogService.Admin.cs

// 執行清除緩存操作
await _blogCacheService.RemoveAsync(CachePrefix.Blog_Post);

因為是小項目,採用這種策略直接刪除緩存,這樣就搞定了當在執行增刪改操作后,前台接口可以實時查詢出最後的結果。

文章詳情

當我們修改文章數據的時候,是需要把當前數據庫中的數據帶出來显示在界面上的,因為有可能只是個別地方需要修改,所以這還需要一個查詢文章詳情的接口,當然這裏的詳情和前端的是不一樣的,這裡是需要根據Id主鍵去查詢。

添加模型類PostForAdminDto.cs,直接繼承PostDto,然後添加一個Tags列表就行,==,好像和上一篇文章中的EditPostInput字段是一模一樣的。順手將EditPostInput改一下吧,具體代碼如下:

//PostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostForAdminDto : PostDto
    {
        /// <summary>
        /// 標籤列表
        /// </summary>
        public IEnumerable<string> Tags { get; set; }
    }
}

//EditPostInput.cs
namespace Meowv.Blog.Application.Contracts.Blog.Params
{
    public class EditPostInput : PostForAdminDto
    {
    }
}

IBlogService.Admin.cs中添加接口。

/// <summary>
/// 獲取文章詳情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<ServiceResult<PostForAdminDto>> GetPostForAdminAsync(int id);

實現這個接口。

/// <summary>
/// 獲取文章詳情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<ServiceResult<PostForAdminDto>> GetPostForAdminAsync(int id)
{
    var result = new ServiceResult<PostForAdminDto>();

    var post = await _postRepository.GetAsync(id);

    var tags = from post_tags in await _postTagRepository.GetListAsync()
               join tag in await _tagRepository.GetListAsync()
               on post_tags.TagId equals tag.Id
               where post_tags.PostId.Equals(post.Id)
               select tag.TagName;

    var detail = ObjectMapper.Map<Post, PostForAdminDto>(post);
    detail.Tags = tags;
    detail.Url = post.Url.Split("/").Where(x => !string.IsNullOrEmpty(x)).Last();

    result.IsSuccess(detail);
    return result;
}

先根據Id查出文章數據,再通過聯合查詢找出標籤數據。

CreateMap<Post, PostForAdminDto>().ForMember(x => x.Tags, opt => opt.Ignore());

新建一條AutoMapper配置,將Post轉換成PostForAdminDto,忽略Tags。

然後將查出來的標籤、Url賦值給DTO,輸出即可。在BlogController.Admin中添加API。

/// <summary>
/// 獲取文章詳情
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
[Authorize]
[Route("admin/post")]
[ApiExplorerSettings(GroupName = Grouping.GroupName_v2)]
public async Task<ServiceResult<PostForAdminDto>> GetPostForAdminAsync([Required] int id)
{
    return await _blogService.GetPostForAdminAsync(id);
}

至此,完成了關於文章的所有接口。

接下來按照以上方式依次完成分類、標籤、友鏈的增刪改查接口,我覺得如果你有跟着我一起做,剩下的可以自己完成。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

搭配下方課程學習更佳 ↓ ↓ ↓

http://gk.link/a/10iQ7

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準

Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(1)——SVM概述,Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(3)——證明SVM,Python機器學習筆記:SVM(4)——sklearn實現

  上一節我學習了完整的SVM過程,下面繼續對核函數進行詳細學習,具體的參考鏈接都在上一篇文章中,SVM四篇筆記鏈接為:

Python機器學習筆記:SVM(1)——SVM概述

Python機器學習筆記:SVM(2)——SVM核函數

Python機器學習筆記:SVM(3)——證明SVM

Python機器學習筆記:SVM(4)——sklearn實現

  熱身實例

  我在上一節有完整的學習了SVM算法,為了不讓自己這麼快就忘了,這裏先學習一個實例,回顧一下,並引出核函數的概念。

  數據是這樣的:有三個點,其中正例 x1(3,  3),x2(4,3),負例 x3(1,1)

   求解:

   約束條件為:

  這個約束條件是通過這個得到的(為什麼這裏強調一下呢,因為我們這個例子本身說的就是SVM的求解過程):

   我們可以知道  y1 = +1, y2 = +1 , y3 = -1,同時,代入 α ,則得到:

  α1 +  α2 – α3=0

  下面通過SVM求解實例。

  我們將數據代入原式中:

   由於 α1 + α2 = α3 化簡可得:

   然後分別對  α1 和 α2 求偏導,偏導等於 0 可得: α1 = 1.5   α2 = -1 ,但是我們發現這兩個解並不滿足約束條件  αi >= 0,i=1,2,3,所以解應該在邊界上(正常情況下,我們需要對上式求偏導,進而算出 w,b)。

  首先令  α1 = 0,得出 α2 = -2/13  ,α3 = -2/13   (不滿足約束)

  再令 α2 = 0,得出 α1 = 0.25  ,α3 = 0.25  (滿足約束)

  所以最小值在(0.25, 0,0.25)處取得。

  我們將 α 的結果代入求解:

   所以我們代入 w  b ,求得最終的平面方程為:

  熱身完畢,下面學習核函數,為了方便理解我們下面要說的核函數,我在知乎找了一個簡單易懂的故事,讓我們了解支持向量機,更是明白支持向量機的核函數是個什麼鬼,下面看故事。

1,故事分析:支持向量機(SVM)是什麼?

  下面故事來源於此(這是源作者鏈接):點擊我即可

  在很久以前的情人節,有一個大俠要去救他的愛人,但是魔鬼和他玩了一個遊戲。

  魔鬼在桌面上似乎有規律放了兩種顏色的球,說:“你用一根棍分開他們?要求:盡量在放更多球之後,仍然使用”

  於是大俠這樣做,幹得不錯吧:

  然後魔鬼又在桌上放了更多的球,似乎有一個球站錯了陣營。

  SVM就是試圖把棍放在最佳位置,好讓在棍的兩邊有盡可能大的間隙。

  現在即使魔鬼放了更多的球,棍仍然是一個好的分界線。

  然後,在SVM工具箱中有另一個更加重要的trick 。魔鬼看到大俠已經學會了一個trick,於是魔鬼給了大俠一個新的挑戰。

  現在,大俠沒有棍可以很好地幫他分開這兩種球了,現在怎麼辦,當然像所有武俠片中一樣大俠桌子一拍,球飛到空中。然後憑藉著大俠的輕功,大俠抓起一張紙,插到了兩種球的中間。

  現在,從魔鬼的角度看這些球,這些球好像是被一條曲線分開了。

  在之後,無聊的大人們,把這些球叫做 「data」,把棍子 叫做 「classifier」, 最大間隙trick 叫做「optimization」, 拍桌子叫做「kernelling」, 那張紙叫做「hyperplane」。

  所以說,Support Vector Machine,一個普通的SVM就是一條直線罷了,用來完美劃分linearly separable的兩類。但是這又不是一條普通的直線,這是無數條可以分類的直線當中最完美的,因為它恰好在兩個類的中間,距離兩個類的點都一樣遠。而所謂的Support vector就是這些離分界線最近的點,如果去掉這些點,直線多半是要改變位置的。如果是高維的點,SVM的分界線就是平面或者超平面。其實沒有差,都是一刀切兩塊,我們這裏統一叫做直線。

  再理解一下,當一個分類問題,數據是線性可分的,也就是用一根棍就可以將兩種小球分開的時候,我們只要將棍的位置放在讓小球距離棍的距離最大化的位置即可。尋找這個最大間隔的過程,就叫做最優化。但是,显示往往是殘酷的,一般的數據是線性不可分的。也就是找不到一個棍將兩種小球很好的分類,這時候我們就需要像大俠一樣,將小球排起,用一張紙代替小棍將兩種小球進行分類,想讓數據飛起,我們需要的東西就是核函數(kernel),用於切分小球的紙,就是超平面。

2,核函數的概念

  上面故事說明了SVM可以處理線性可分的情況,也可以處理非線性可分的情況。而處理非線性可分的情況是選擇了核函數(kernel),通過將數據映射到高位空間,來解決在原始空間中線性不可分的問題。

  我們希望樣本在特徵空間中線性可分,因此特徵空間的好壞對支持向量機的性能至關重要,但是在不知道特徵映射的情況下,我們是不知道什麼樣的核函數是適合的,而核函數也只是隱式的定義了特徵空間,所以,核函數的選擇對於一個支持向量機而言就顯得至關重要,若選擇了一個不合適的核函數,則數據將映射到不合適的樣本空間,從而支持向量機的性能將大大折扣。

  所以構造出一個具有良好性能的SVM,核函數的選擇是關鍵。而核函數的選擇包含兩部分工作:一是核函數類型的選擇,二是確定核函數類型后相關參數的選擇。

  我們知道,核函數的精妙之處在於不用真的對特徵向量做核映射,而是直接對特徵向量的內積進行變換,而這種變換卻等價於先對特徵向量做核映射然後做內積。

  SVM主要是在用對偶理論求解一個二次凸優化問題,其中對偶問題如下:

   求得最終結果:

  當然這是線性可分的情況,那麼如果問題本身是線性不可分的情況呢?那就是先擴維后再計算。具體來說,在線性不可分的情況下,支持向量機首先在低維空間中完成計算,然後通過核函數將輸入空間映射到高維特徵空間,最終在高維特徵空間中構造出最優分離超平面,從而把平面上本身不好分的非線性數據分開。如下圖所示,一堆數據在二維空間中無法劃分,從而映射到三維空間中劃分:

   而在我們遇到核函數之前,如果用原始的方法,那麼在用線性學習器學習一個非線性關係,需要選擇一個非線性特徵集,並且將數據寫成新的表達形式,這等價於應用一個固定的非線性映射,將數據映射到特徵空間,在特徵空間中使用線性學習器。因此,考慮的假設集是這種類型的函數:

   這裏 Φ :X -> F 是從輸入空間到某個特徵空間的映射,這意味着建立非線性學習器分為兩步:

  • 1,使用一個非線性映射將數據變換到一個特徵空間 F
  • 2,在特徵空間使用線性學習器分類

  而由於對偶形式就是線性學習器的一個重要性質,這意味着假設可以表達為訓練點的線性組合,因此決策規則可以用測試點和訓練點的內積來表示:

  為向量加上核映射后,要求解的最優化問題變為:

  根據核函數滿足的等式條件,它等價於下面的問題:

  其線性不可分情況的對偶形式如下:

   其中 Φ(xi) 表示原來的樣本擴維后的坐標。

  最後得到的分類判別函數為:

   和不用核映射相比,只是求解的目標函數,最後的判定函數對特徵向量的內積做了核函數變換。如果K是一個非線性函數,上面的決策函數則是非線性函數,此時SVM是非線性模型。當訓練樣本很多,支持向量的個數很大的時候,預測時的速度是一個問題,因此很多時候我們會使用線性支持向量機。

3,舉例說明核函數的巧妙之處

  下面先從一個小例子來闡述問題。假設我們有兩個數據, x = (x1,  x2,  x3)  y = (y1,  y2,  y3)。此時在3D空間已經不能對其進行線性劃分了,那麼我們通過一個函數將數據映射到更高維的空間,比如9維的話,那麼 f(x) = (x1x1, x1x2, x1x3, x2x1, x2x2, x2x3, x3x1, x3x2, x3x3),由於需要計算內積,所以在新的數據在 9 維空間,需要計算  <f(x),  f(y)> 的內積,需要花費 O(n^2)。

  再具體點,令 x = (1, 2, 3), y = (4, 5, 6),那麼 f(x)  = (1, 2, 3, 2, 4, 6, 3, 6, 9),f(y) = (16, 20, 24, 20, 25, 36, 24, 30, 36)

  此時: <f(x),  f(y)>  = 16 + 40 + 72 +40 +100 + 180 + 72 +180 +324 = 1024

  對於3D空間這兩個數據,似乎還能計算,但是如果將維數擴大到一個非常大數的時候,計算起來可就不是這麼一點點問題了。

  然而,我們發現  K(x, y) = (<x, y>)^2   ,代入上式: K(x, y) = (4 + 10 + 18)^2 = 32^2 = 1024

  也就是說 : K(x, y) = (<x, y>)^2  = <f(x),  f(y)>

  但是 K(x, y) 計算起來卻比 <f(x), f(y)> 簡單的多,也就是說只要用 K(x, y)來計算,效果與 <f(x), f(y)> 是一樣的,但是計算效率卻大幅度提高了,如  K(x, y) 是 O(n),而  <f(x), f(y)> 是 O(n^2),所以使用核函數的好處就是,可以在一個低維空間去完成一個高緯度(或者無限維度)樣本內積的計算,比如上面例子中 K(x, y)的3D空間對比 <f(x), f(y)> 的9D空間。

  下面再舉個例子來證明一下上面的問題,為了簡單起見,假設所有樣本點都是二維點,其值分別為(x,  y),分類函數為:

   它對應的映射方式為:

   可以驗證:任意兩個擴維后的樣本點在3維空間的內積等於原樣本點在二維空間的函數輸出

   有了這個核函數,以後的高維內積都可以轉換為低維的函數運算了,這裏也就是說只需要計算低維的內積,然後再平方。明顯問題得到解決且複雜度降低極大。總而言之:核函數它本質上隱含了從低維到高維的映射,從而避免直接計算高維的內積

  當然上述例子是多項式核函數的一個特例,其實核函數的種類還有很多,後文會一一介紹。

4,核函數的計算原理

  通過上面的例子,我們大概可以知道核函數的巧妙應用了,下面學習一下核函數的計算原理。

  如果有一種方法可以在特徵空間中直接計算內積  <Φ(xi , Φ(x)> ,就像在原始輸入點的函數中一樣,就有可能將兩個步驟融合到一起建立一個非線性的學習器,這樣直接計算的方法稱為核函數方法。

  設 x 是輸入空間(歐式空間或者離散集合),H為特徵空間(希爾伯特空間),如果存在一個從 x 到 H 的映射:

  核是一個函數 K,對於所有 x, z ∈ χ, 則滿足:

   則稱Κ(x,z)為核函數,φ(x)為映射函數,φ(x)∙φ(z)為x,z映射到特徵空間上的內積。

  參考網友的理解:任意兩個樣本點在擴維后的空間的內積,如果等於這兩個樣本點在原來空間經過一個函數后的輸出,那麼這個函數就叫核函數

  由於映射函數十分複雜難以計算,在實際中,通常都是使用核函數來求解內積,計算複雜度並沒有增加,映射函數僅僅作為一種邏輯映射,表徵着輸入空間到特徵空間的映射關係。至於為什麼需要映射后的特徵而不是最初的特徵來參与計算,為了更好地擬合是其中一個原因,另外的一個重要原因是樣例可能存在線性不可分的情況,而將特徵映射到高維空間后,往往就可分了。

  下面將核函數形式化定義。如果原始特徵內積是 <X ,  Z>,映射 <Φ(xi • Φ(x)>,那麼定義核函數(Kernel)為:

  到這裏,我們可以得出結論,如果要實現該節開頭的效果,只需要計算 Φ(x) ,然後計算 Φ(x)TΦ(x)即可,然而這種計算方式是非常低效的。比如最初的特徵是n維的,我們將其映射到 n2 維,然後再計算,這樣需要O(n2 ) 的時間,那麼我們能不能想辦法減少計算時間呢?

  先說結論,當然是可以的,畢竟我們上面例子,活生生的說明了一個將需要 O(n2 ) 的時間 轉換為 需要O(n ) 的時間。

  先看一個例子,假設x和z都是n維度的,

  展開后,得到:

  這個時候發現我們可以只計算原始特徵 x 和 z 內積的平方(時間複雜度為O(n)),就等價於計算映射后特徵的內積。也就是說我們不需要花O(n2 ) 的時間了。

  現在看一下映射函數(n = 3),根據上面的公式,得到:

  也就是說核函數  Κ(x,z) = (xTz)2  只能選擇這樣的 φ 作為映射函數時才能夠等價於映射后特徵的內積

  再看另外一個核函數,高斯核函數:

  這時,如果 x 和 z 很相近 (||x – z || 約等於 0),那麼核函數值為1,如果 x 和 z 相差很大(||x – z ||  >> 0),那麼核函數值約等於0.由於這個函數類似於高斯分佈,因此稱為高斯核函數,也叫做徑向基函數(Radial Basis Function 簡稱為RBF)。它能夠把原始特徵映射到無窮維。

  下面有張圖說明在低維線性不可分時,映射到高維后就可分了,使用高斯核函數。

  注意,使用核函數后,怎麼分類新來的樣本呢?線性的時候我們使用SVM學習出w和b,新來樣本x的話,我們使用 wTx + b 來判斷,如果值大於等於1,那麼是正類,小於等於是負類。在兩者之間,認為無法確定。如果使用了核函數后,wTx + b 就變成了 wTΦ(x) + b,是否先要找到 Φ(x) ,然後再預測?答案肯定不是了,找 Φ(x) 很麻煩,回想我們之前說過的。

  只需將 <(x(i) , x> 替換成  (x(i) , x),然後值的判斷同上。

4.1  核函數有效性的判定

  問題:給定一個函數K,我們能否使用K來替代計算 Φ(x)TΦ(x),也就說,是否能夠找出一個 Φ,使得對於所有的x和z,都有 K(x, z) = Φ(x)TΦ(x),即比如給出了 K(x, z) = (xTz)2,是否能夠認為K是一個有效的核函數。

  下面來解決這個問題,給定m個訓練樣本(x(1),x(2), ….,x(m)),每一個x(i) 對應一個特徵向量。那麼,我們可以將任意兩個 x(i) 和 x(j) 帶入K中,計算得到Kij = K(x(i), x(j))。i 可以從1到m,j 可以從1到m,這樣可以計算出m*m的核函數矩陣(Kernel Matrix)。為了方便,我們將核函數矩陣和 K(x, z) 都使用 K 來表示。如果假設 K 是有效地核函數,那麼根據核函數定義:

  可見,矩陣K應該是個對稱陣。讓我們得出一個更強的結論,首先使用符號ΦK(x)來表示映射函數 Φ(x) 的第 k 維屬性值。那麼對於任意向量 z,得:

  最後一步和前面計算 K(x, z) = (xTz)2 時類似。從這個公式我們可以看出,如果K是個有效的核函數(即 K(x, z)   Φ(x)TΦ(z)等價),那麼,在訓練集上得到的核函數矩陣K應該是半正定的(K>=0)。這樣我們得到一個核函數的必要條件:K是有效的核函數 ==> 核函數矩陣K是對稱半正定的。

  Mercer定理表明為了證明K是有效的核函數,那麼我們不用去尋找 Φ ,而只需要在訓練集上求出各個 Kij,然後判斷矩陣K是否是半正定(使用左上角主子式大於等於零等方法)即可。

 

5,核函數:如何處理非線性數據

  來看個核函數的例子。如下圖所示的兩類數據,分別分佈為兩個圓圈的形狀,這樣的數據本身就是線性不可分的,此時我們該如何把這兩類數據分開呢?

   事實上,上圖所示的這個數據集,是用兩個半徑不同的圓圈加上了少量的噪音生成得到的,所以,一個理想的分界應該是“圓圈” 而不是“一條線”(超平面)。如果用 X1 和 X2 來表示這個二維平面的兩個坐標的話,我們知道一條二次曲線(圓圈是二次曲線的一種特殊情況)的方程可以寫作這樣的形式:

   注意上面的形式,如果我們構造另外一個五維的空間,其中五個坐標的值分別為:

   那麼顯然,上面的方程在新的坐標系下可以寫做:

   關於新的坐標 Z,這正是一個 hyper plane 的方程!也就是說,如果我們做一個映射:

   將X按照上面的規則映射為 Z,那麼在新的空間中原來的數據將變成線性可分的,從而使用之前我們推導的線性分類算法就可以進行處理了。這正是Kernel方法處理非線性問題的基本思想。

  再進一步描述 Kernel 的細節之前,不妨再來看看上述例子在映射過後的直觀形態。當然,我們無法將五維空間畫出來,不過由於我這裏生成數據的時候用了特殊的情形,所以這裏的超平面實際的方程是這個樣子的(圓心在X2軸上的一 個正圓):

   因此我只需要把它映射到下面這樣一個三維空間中即可:

   下圖即是映射之後的結果,將坐標軸經過適當的旋轉,就可以很明顯的看出,數據是可以通過一個平面來分開的

  核函數相當於把原來的分類函數:

   映射成:

   而其中的 α 可以通過求解如下 dual 問題而得到的:

   這樣一來問題就解決了嗎?似乎是的:拿到非線性數據,就找一個映射(Φ(•),然後一股腦把原來的數據映射到新空間中,再做線性SVM即可。不過事實上問題好像沒有這麼簡單)。

  細想一下,剛才的方法是不是有問題:

  在最初的例子里,我們對一個二維空間做映射,選擇的新空間是原始空間的所有一階和二階的組合,得到了五個維度;

  如果原始空間是三維(一階,二階和三階的組合),那麼我們會得到:3(一次)+3(二次交叉)+3(平方)+3(立方)+1(x1 * x2 * x3) + 2*3(交叉,一個一次一個二次,類似 x1*x2^2)=19 維的新空間,這個數目是呈指數級爆炸性增長的,從而勢必這給 Φ(•) 的計算帶來非常大的困難,而且如果遇到無窮維的情況,就根本無從計算了。

  這個時候,可能就需要Kernel出馬了。

  不妨還是從最開始的簡單例子觸發,設兩個向量為:

   而 Φ(•) 即是前面說的五維空間的映射,因此映射過後的內積為:

   (公式說明:上面的這兩個推導過程中,所說的前面的五維空間的映射,這裏說的便是前面的映射方式,回顧下之前的映射規則,再看看那個長的推導式,其實就是計算x1,x2各自的內積,然後相乘相加即可,第二個推導則是直接平方,去掉括號,也很容易推出來)

  另外,我們又注意到:

   二者有很多相似的地方,實際上,我們只要把某幾個維度線性縮放一下,然後再加上一個常數維度,具體來說,上面這個式子的計算結果實際上和映射

   之後的內積  <Φ(xi • Φ(x)>  的結果是相等的,那麼區別在什麼地方呢?

  • 1,一個是映射到高維空間中,然後再根據內積的公式進行計算
  • 2,另一個則直接在原來的低維空間中進行計算,而不需要顯式地寫出映射后的結果

  (公式說明:上面之中,最後的兩個式子,第一個算式,是帶內積的完全平方式,可以拆開,然後,再通過湊一個得到,第二個算式,也是根據第一個算式湊出來的)

  回想剛才提到的映射的維度爆炸,在前一種方法已經無法計算的情況下,后一種方法卻依舊能從容處理,甚至是無窮維度的情況也沒有問題。

  我們把這裏的計算兩個向量在隱式映射過後的空間中的內積的函數叫做核函數(kernel Function),例如,在剛才的例子中,我們的核函數為:

   核函數能簡化映射空間中的內積運算——剛好“碰巧”的是,在我們的SVM里需要計算的地方數據向量總是以內積的形式出現的。對比剛才我們上面寫出來的式子,現在我們的分類函數為:

   其中 α 由如下 dual 問題計算而得:

  這樣一來計算的問題就算解決了,避免了直接在高維空間中進行計算,而結果卻是等價的!當然,因為我們這裏的例子非常簡單,所以可以手工構造出對應於 Φ(•) 的核函數出來,如果對於任意一個映射,想要構造出對應的核函數就非常困難了。

6,核函數的本質

  下面概況一下核函數的意思:

  • 1,實際上,我們會經常遇到線性不可分的樣例,此時,我們的常用做法是把樣例特徵映射到高位空間中去(比如之前有個例子,映射到高維空間后,相關特徵便被分開了,也就達到了分類的目的)
  • 2,進一步,如果凡是遇到線性不可分的樣例,一律映射到高維空間,那麼這個維度大小是會高到可怕的(甚至是無窮維),所以核函數就隆重出場了,核函數的價值在於它雖然也是將特徵進行從低維到高維的轉換,但核函數絕就絕在它事先在低維上進行計算,而將實質上的分類效果表現在了高維上,也就是上文所說的避免了直接在高維空間中的複雜計算。

  下面引用這個例子距離下核函數解決非線性問題的直觀效果。

  假設現在你是一個農場主,圈養了一批羊群,但為了預防狼群襲擊羊群,你需要搭建一個籬笆來把羊群圈起來。但是籬笆應該建在哪裡呢?你很可能需要依據羊群和狼群的位置搭建一個“分類器”,比如下圖這幾種不同的分類器,我們可以看到SVM完成了一個很完美的解決方案。

   這個例子側面簡單說明了SVM使用非線性分類器的優勢,而邏輯模式以及決策樹模式都是使用了直線方法。

7,幾種常見的核函數

  核函數有嚴格的數學要求,所以設計一個核函數是非常困難的,科學家們經過很多很多嘗試也就只嘗試出來幾個核函數,所以我們就不要在這方面下無用功了,直接拿這常見的幾個核函數使用就OK。

  下面來分別學習一下這幾個常見的核函數。

7.1  線性核(Linear Kernel )

  基本原理:實際上就是原始空間中的內積

   這個核存在的主要目的是使得“映射后空間中的問題” 和 “映射前空間中的問題” 兩者在形式上統一起來了(意思是說:我們有的時候,寫代碼或者寫公式的時候,只要寫個模板或者通用表達式,然後再代入不同的核,便可以了。於此,便在形式上統一了起來,不用再找一個線性的和一個非線性的)

     線性核,主要用於線性可分的情況,我們可以看到特徵空間到輸入空間的維度是一樣的。在原始空間中尋找最優線性分類器,具有參數少速度快的優勢。對於線性可分數據,其分類效果很理想。因此我們通常首先嘗試用線性核函數來做分類,看看效果如何,如果不行再換別的。

優點

  • 方案首選,奧多姆剃刀定理
  • 簡單,可以快速解決一個QP問題
  • 可解釋性強:可以輕易知道哪些feature是重要的

限制

  • 只能解決線性可分問題

7.2 多項式核(Polynomial Kernel)

  基本原理:依靠升維使得原本線性不可分的數據線性可分。

  多項式核函數可以實現將低維的輸入空間映射到高維的特徵空間。多項式核適合於正交歸一化(向量正交且模為1)數據,屬於全局核函數,允許相距很遠的數據點對核函數的值有影響。參數d越大,映射的維度越高,計算量就會越大。

優點

  • 可解決非線性問題
  • 可通過主觀設置Q來實現總結的預判

缺點

  • 多項式核函數的參數多,當多項式的階數d比較高的是,由於學習複雜性也會過高,易出現“過擬合”現象,核矩陣的元素值將趨於無窮大或者無窮小,計算複雜度會大道無法計算。

 

7.3  高斯核(Gaussian Kernel)/ 徑向基核函數(Radial Basis Function)

  徑向基核函數是SVM中常用的一個核函數。徑向基函數是一個採用向量作為自變量的函數,能夠基於向量距離運算輸出一個標量。

   也可以寫成如下格式:

  徑向基函數是指取值僅僅依賴於特定點距離的實值函數,也就是:

  任意一個滿足上式特性的函數 Φ 都叫徑向量函數,標準的一般使用歐氏距離,儘管其他距離函數也是可以的。所以另外兩個比較常用的核函數,冪指數核,拉普拉斯核也屬於徑向基核函數。此外不太常用的徑向基核還有ANOVA核,二次有理核,多元二次核,逆多元二次核。

  高斯徑向基函數是一種局部性強的核函數,其可以將一個樣本映射到一個更高維的空間內,該核函數是應用最廣的一個,無論大樣本還是小樣本都有比較好的性能,而且其相對於多項式核函數參數要少,因此大多數情況下在不知道用什麼樣的核函數的時候優先使用高斯核函數

  徑向基核函數屬於局部核函數,當數據點距離中心點變遠時,取值會變小。高斯徑向基核對數據中存在的噪聲有着較好的抗干擾能力,由於其很強的局部性,其參數決定了函數作用範圍,隨着參數 σ 的增大而減弱。

優點

  • 可以映射到無線維
  • 決策邊界更為多樣
  • 只有一個參數,相比多項式核容易選擇

缺點

  • 可解釋性差(無限多維的轉換,無法算出W)
  • 計算速度比較慢(當解決一個對偶問題)
  • 容易過擬合(參數選不好時容易overfitting)

上述所講的徑向核函數表達式

  冪指數核(Exponential Kernel)

  拉普拉斯核(LaplacIan Kernel)

 

   ANOVA 核(ANOVA Kernel)

  二次有理核(Rational Quadratic Kernel)

  多元二次核(Multiquadric Kernel)

  逆多元二次核(Inverse Multiquadric Kernel)

7.4  Sigmoid核

   Sigmoid核函數來源於神經網絡,被廣泛用於深度學習和機器學習中

  採用Sigmoid函數作為核函數時,支持向量機實現的就是一種多層感知器神經網絡,應用SVM方法,隱含層節點數目(它確定神經網絡的結構),隱含層節點對輸入節點的權重都是在設計(訓練)的過程中自動確定的。而且支持向量機的理論基礎決定了它最終求得的是全局最優值而不是局部最優值,也保證了它對未知樣本的良好泛化能力而不會出現過學習線性。

8,核函數的選擇

8.1,先驗知識

  利用專家的先驗知識預先選定核函數

8.2,交叉驗證

  採取Cross-Validation方法,即在進行核函數選取時,分別試用不同的核函數,歸納誤差最小的核函數就是最好的核函數。如針對傅里恭弘=叶 恭弘核,RBF核,結合信號處理問題中的函數回歸問題,通過仿真實驗,對比分析了在相同數據條件下,採用傅里恭弘=叶 恭弘核的SVM要比採用RBF核的SVM誤差小很多。

8.3,混合核函數

  採用由Smits等人提出的混合核函數方法,該方法較之前兩者是目前選取核函數的主流方法,也是關於如何構建核函數的又一開創性的工作,將不同的核函數結合起來後有更好的特性,這是混合核函數方法的基本思想。

8.4,經驗

  當樣本特徵很多時,特徵的維度很高,這是往往樣本線性可分,可考慮用線性核函數的SVM或者LR(如何不考慮核函數,LR和SVM都是線性分類算法,也就是說他們的分類決策面都是線性的)

  當樣本的數量很多,但特徵較少時,可以手動添加一些特徵,使樣本線性可分,再考慮用線性核函數的SVM或者LR

  當樣本特徵維度不高時,樣本數量也不多時,考慮使用高斯核函數(RBF核函數的一種,指數核函數和拉普拉斯核函數也屬於RBF核函數)

8.5,吳恩達給出的選擇核函數的方法

   如果特徵的數量大道和樣本數量差不多,則選用LR或者線性核的SVM

  如果特徵的數量小,樣本的數量正常,則選用SVM+ 高斯核函數

  如果特徵的數量小,而樣本的數量很大,則需要手工添加一些特徵從而變成第一種情況

8.6  核函數選擇的例子

  這裏簡單說一下核函數與其他參數的作用(後面會詳細學習關於使用Sklearn學習SVM):

  • kernel=’linear’ 時,C越大分類效果越好,但有可能會過擬合(default C=1)
  • kernel=’rbf’時,為高斯核,gamma值越小,分類界面越連續;gamma值越大,分類界面越“散”,分類效果越好,但有可能會過擬合。

  我們來看一個簡單的例子,數據為[-5.0 , 9.0] 的隨機數組,函數如下 :

  下面分別使用三種核SVR:兩種乘法係數高斯核rbf和一種多項式核poly。代碼如下:

from sklearn import svm
import numpy as np
from matplotlib import pyplot as plt
import warnings

warnings.filterwarnings('ignore')

X = np.arange(-5.0 , 9.0 , 0.1)
# print(X)
X = np.random.permutation(X)
# print('1X:',X)
X_ = [[i] for i in X]
b = 0.5
y = 0.5 * X ** 2.0 + 3.0 * X + b + np.random.random(X.shape) * 10.0
y_ = [i for i in y]

# degree = 2 , gamma=, coef0 =
rbf1 = svm.SVR(kernel='rbf',C=1,)
rbf2 = svm.SVR(kernel='rbf',C=20,)
poly = svm.SVR(kernel='poly',C=1,degree=2)

rbf1.fit(X_ , y_)
rbf2.fit(X_ , y_)
poly.fit(X_ , y_)


result1 = rbf1.predict(X_)
result2 = rbf2.predict(X_)
result3 = poly.predict(X_)


plt.plot(X,y,'bo',fillstyle='none')
plt.plot(X,result1,'r.')
plt.plot(X,result2,'g.')
plt.plot(X,result3,'b.')
plt.show()

  結構圖如下:

  藍色是poly,紅色是c=1的rbf,綠色c=20的rbf。其中效果最好的是C=20的rbf。如果我們知道函數的表達式,線性規劃的效果更好,但是大部分情況下我們不知道數據的函數表達式,因此只能慢慢實驗,SVM的作用就在這裏了。

9,總結

  支持向量機是一種分類器。之所以稱為“機”是因為它會產生一個二值決策結果,即它是一種決策“機”。支持向量機的泛化錯誤率較低,也就是說它具有良好的學習能力,且學到的結果具有很好的推廣性。這些優點使得支持向量機十分流行,有些人認為它是監督學習中最好的定式算法。

  支持向量機視圖通過求解一個二次優化問題來最大化分類間隔。在過去,訓練支持向量機常採用非常複雜並且低效的二次規劃求解方法。John Platt 引入了SMO算法,此算法可以通過每次只優化2個 α 值來加快SVM的訓練速度。

  核方法或者說核技巧會將數據(有時候是非線性數據)從一個低維空間映射到一個高維空間,可以將一個在低維空間中的非線性問題轉化為高維空間下的線性問題來求解。核方法不止在SVM中適用,還可以用於其他算法。而其中的徑向基函數是一個常用的度量兩個向量距離的核函數。

 

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

美研究早預言武漢肺炎 點名「野味市場」風險最高

摘錄自2020年4月8日自由時報報導

根據《CNN》報導,美國加州大學(University of California)與澳洲墨爾本大學(University of Melbourne)研究人員數年前進行的研究中發現,在人類持續開發破壞自然棲地時,原宿主是動植物的病毒一旦得以傳染人類,將構成相當高程度的風險。

研究作者,美國加州大學流行病學教授強森博士(Dr. Christine Kreuder Johnson)當時便曾警告,未來很可能出現具有威脅人類生存能力的動物傳人傳染病。

強森博士表示,當人類把野生哺乳類動物從自然環境捕捉並運送至市場上出售時,對這些活體野生動物造成極大的壓力,導致更多潛在存於動物體內的病毒有更高的潛力散播至人群中。她說,與野外接觸相比,處於被捕獲狀態下的野生動物具有更高的排泄、噴沫等衝動,將導致病毒被大量排出,暴露在大量人群密集分布的環境,如中國惡名昭彰的野味市場。

生活環境
國際新聞
美國
野味
武漢肺炎
蝙蝠與新興傳染病
食品安全

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

疫情之後還有氣候戰役 歐盟十國環境部長公開信:復甦方案要綠色政綱

環境資訊中心綜合外電;黃鈺婷、鄒敏惠 編譯;趙家緯 審校

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

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

趁疫情偷抓瀕危鱘魚 野生動物犯罪急升 WWF警告:中東歐保育類猛禽陷危機

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

※回頭車貨運收費標準

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

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

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

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理

前文傳送門:

「Python 圖像處理 OpenCV (1):入門」

「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 显示圖像」

「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

「Python 圖像處理 OpenCV (5):圖像的幾何變換」

「Python 圖像處理 OpenCV (6):圖像的閾值處理」

1. 引言

第一件事情還是先做名詞解釋,圖像平滑到底是個啥?

從字面意思理解貌似圖像平滑好像是在說圖像滑動。

emmmmmmmmmmmmmmm。。。。

其實半毛錢關係也沒有,圖像平滑技術通常也被成為圖像濾波技術(這個名字看到可能大家會有點感覺)。

每一幅圖像都包含某種程度的噪聲,噪聲可以理解為由一種或者多種原因造成的灰度值的隨機變化,如由光子通量的隨機性造成的噪聲等等。

而圖像平滑技術或者是圖像濾波技術就是用來處理圖像上的噪聲,其中,能夠具備邊緣保持作用的圖像平滑處理,成為了大家關注的重點。

這不廢話,處理個圖片降噪,結果把整個圖像搞的跟玻璃上糊上了一層水霧一樣,這種降噪有啥意義。

本文會介紹 OpenCV 中提供的圖像平滑的 4 個算法:

  • 均值濾波
  • 方框濾波
  • 高斯濾波
  • 中值濾波

下面開始一個一個看吧:)

先給出一個給馬里奧加噪聲的程序,程序來源於楊老師的博客:https://blog.csdn.net/Eastmount/article/details/82216380 ,完整代碼如下:

import cv2 as cv
import numpy as np

# 讀取圖片
img = cv.imread("maliao.jpg", cv.IMREAD_UNCHANGED)
rows, cols, chn = img.shape

# 加噪聲
for i in range(5000):
    x = np.random.randint(0, rows)
    y = np.random.randint(0, cols)
    img[x, y, :] = 255

cv.imshow("noise", img)

# 圖像保存
cv.imwrite("maliao_noise.jpg", img)

# 等待显示
cv.waitKey()
cv.destroyAllWindows()

上面這段程序實際上是在圖片上隨機加了 5000 個白點,這個噪聲真的是夠大的了。

2. 2D 圖像卷積

在介紹濾波之前先簡單介紹下 2D 圖像卷積,圖像卷積其實就是圖像過濾。

圖像過濾的時候可以使用各種低通濾波器( LPF ),高通濾波器( HPF )等對圖像進行過濾。

低通濾波器( LPF )有助於消除噪聲,但是會使圖像模糊。

高通濾波器( HPF )有助於在圖像中找到邊緣。

OpenCV 為我們提供了一個函數 filter2D() 來將內核與圖像進行卷積。

我們嘗試對圖像進行平均濾波, 5 x 5 平均濾波器內核如下:

\[ K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \]

具體操作如下:

我們保持這個內核在一個像素上,將所有低於這個內核的 25 個像素相加,取其平均值,然後用新的平均值替換中心像素。它將對圖像中的所有像素繼續此操作,完整的示例代碼如下:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

# 讀取圖片
img = cv.imread("maliao_noise.jpg", cv.IMREAD_UNCHANGED)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

kernel = np.ones((5,5),np.float32)/25

dst = cv.filter2D(rgb_img, -1, kernel)

titles = ['Source Image', 'filter2D Image']
images = [rgb_img, dst]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

可以看到,噪點確實去除掉了,就是圖片變得模糊起來。

3. 均值濾波

均值濾波是指任意一點的像素值,都是周圍 N * M 個像素值的均值。

其實均值濾波和上面的那個圖像卷積的示例,做了同樣的事情,我只是用 filter2D() 這個方法手動完成了均值濾波,實際上 OpenCV 為我們提供了專門的均值濾波的方法,前面圖像卷積沒有看明白的同學,可以再一遍均值濾波,我盡量把這個事情整的明白的。

還是來畫個圖吧:

中間那個紅色的方框裏面的值,是周圍 25 個格子區域中的像素的和去除以 25 ,這個公式是下面這樣的:

\[ K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \]

我為了偷懶,所有的格子裏面的像素值都寫成 1 ,畢竟 n / n 永遠都等於 1 ,快誇我機智。

上面這個 5 * 5 的矩陣稱為核,針對原始圖像內的像素點,採用核進行處理,得到結果圖像。

這個核我們可以自定義大小,比如 5 * 5 ,3 * 3 , 10 * 10 等等,具體定義多大完全看療效。

OpenCV 為我提供了 blur() 方法用作實現均值濾波,原函數如下:

def blur(src, ksize, dst=None, anchor=None, borderType=None)
  • kSize: 內核參數,其實就是圖片進行卷積的時候相乘的那個矩陣,具體的卷積是如何算的,網上有很多,我這裏就不介紹了,所得到的圖像是模糊的,而且圖像其實是按照原來的比例缺少了(原圖像-內核參數+1)^2 個單元格。
  • anchor: Point 類型,即錨點,有默認值 Point(-1, -1) ,當坐標為負值,就表示取核的中心。
  • borderType: Int 類型,用於推斷圖像外部像素的某種邊界模式,有默認值 BORDER_DEFAULT 。

接下來是均值濾波的示例代碼:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread("maliao_noise.jpg", cv.IMREAD_UNCHANGED)
rgb_img = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 均值濾波
blur_img = cv.blur(rgb_img, (3, 3))
# blur_img = cv.blur(img, (5, 5))
# blur_img = cv.blur(img, (10, 10))
# blur_img = cv.blur(img, (20, 20))

titles = ['Source Image', 'Blur Image']
images = [rgb_img, blur_img]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

這個降噪的效果好像沒有前面 2D 卷積的那個降噪效果好,但是圖像更為清晰,因為我在這個示例中使用了更小的核 3 * 3 的核,順便我也試了下大核,比如代碼中註釋掉的 10 * 10 的核或者 20 * 20 的核,實時證明,核越大降噪效果越好,但是相反的是圖像會越模糊。

4. 方框濾波

方框濾波和均值濾波核基本一致,其中的區別是需不需要進行歸一化處理。

什麼是歸一化處理等下再說,我們先看方框濾波的原函數:

def boxFilter(src, ddepth, ksize, dst=None, anchor=None, normalize=None, borderType=None)
  • src: 原始圖像。
  • ddepth: Int 類型,目標圖像深度,通常用 -1 表示與原始圖像一致。
  • kSize: 內核參數。
  • dst: 輸出與 src 大小和類型相同的圖像。
  • anchor: Point 類型,即錨點,有默認值 Point(-1, -1) 。
  • normalize: Int 類型,表示是否對目標圖像進行歸一化處理。

當 normalize 為 true 時,需要執行均值化處理。

當 normalize 為 false 時,不進行均值化處理,實際上是求周圍各像素的和,很容易發生溢出,溢出時均為白色,對應像素值為 255 。

完整示例代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread('maliao_noise.jpg')
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 方框濾波
result = cv.boxFilter(source, -1, (5, 5), normalize = 1)

# 显示圖形
titles = ['Source Image', 'BoxFilter Image']
images = [source, result]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

當我們把 normalize 的屬性設為 0 時,不進行歸一化處理,結果就變成了下面這個樣子:

5. 高斯濾波

為了克服簡單局部平均法的弊端(圖像模糊),目前已提出許多保持邊緣、細節的局部平滑算法。它們的出發點都集中在如何選擇鄰域的大小、形狀和方向、參數加平均及鄰域各店的權重係數等。

在高斯濾波的方法中,實際上是把卷積核換成了高斯核,那麼什麼是高斯核呢?

簡單來講就是方框還是那個方框,原來每個方框裏面的權是相等的,大家最後取平均,現在變成了高斯分佈的,方框中心的那個權值最大,其餘方框根據距離中心元素的距離遞減,構成一個高斯小山包,這樣取到的值就變成了加權平均。

下圖是所示的是 3 * 3 和 5 * 5 領域的高斯核。

高斯濾波是在 OpenCV 中是由 GaussianBlur() 方法進行實現的,它的原函數如下:

def GaussianBlur(src, ksize, sigmaX, dst=None, sigmaY=None, borderType=None)
  • sigmaX: 表示 X 方向方差。

這裏需要注意的是 ksize 核大小,在高斯核當中,核 (N, N) 必須是奇數, X 方向方差主要控制權重。

完整的示例代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread('maliao_noise.jpg')
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 方框濾波
result = cv.GaussianBlur(source, (3, 3), 0)

# 显示圖形
titles = ['Source Image', 'GaussianBlur Image']
images = [source, result]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

6. 中值濾波

在使用鄰域平均法去噪的同時也使得邊界變得模糊。

而中值濾波是非線性的圖像處理方法,在去噪的同時可以兼顧到邊界信息的保留。

中值濾波具體的做法是選一個含有奇數點的窗口 W ,將這個窗口在圖像上掃描,把窗口中所含的像素點按灰度級的升或降序排列,取位於中間的灰度值來代替該點的灰度值。

下圖是一個一維的窗口的濾波過程:

在 OpenCV 中,主要是通過調用 medianBlur() 來實現中值濾波,它的原函數如下:

def medianBlur(src, ksize, dst=None)

中值濾波的核心數和高斯濾波的核心數一樣,必須要是大於 1 的奇數。

示例代碼如下:

import cv2 as cv
import matplotlib.pyplot as plt

# 讀取圖片
img = cv.imread('maliao_noise.jpg')
source = cv.cvtColor(img, cv.COLOR_BGR2RGB)

# 方框濾波
result = cv.medianBlur(source, 3)

# 显示圖形
titles = ['Source Image', 'medianBlur Image']
images = [source, result]

for i in range(2):
    plt.subplot(1, 2, i + 1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])

plt.show()

可以明顯看到,目前中值濾波是對原圖像降噪后還原度最高的,常用的中值濾波的圖形除了可以使用方框,還有十字形、圓形和環形,不同形狀的窗口產生不同的濾波效果。

方形和圓形窗口適合外輪廓線較長的物體圖像,而十字形窗口對有尖頂角狀的圖像效果好。

對於一些細節較多的複雜圖像,可以多次使用不同的中值濾波。

7. 示例代碼

如果有需要獲取源碼的同學可以在公眾號回復「OpenCV」進行獲取。

8. 參考

https://blog.csdn.net/Eastmount/article/details/82216380

http://www.woshicver.com/

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

搞清楚C語言指針

Part 0:為什麼要寫這篇文章

C語言中的指針是C語言的精髓,也是C語言的重難點之一。
然而,很少有教程能把指針講的初學者能聽懂,還不會引起歧義。
本文章會嘗試做到這一點,如有錯誤,請指出。

Part 1:地址和&

我們先拋開指針不談,來講一個小故事:

一天,小L準備去找小S玩。但是小L不知道小S的家住在哪裡,正當他着急的時候,他看到了一個路牌,上面寫着:小S的家在神仙小區403

哦,真的是要素過多。為什麼這麼說?

  1. 小L和小S:我們可以看做是兩個變量/常量。
  2. 小S的家:這裏可以看做是變量/常量小S的地址。
    我們要搞清楚,每個變量/常量都和我們一樣:我們每個人都有自己的家,正如變量也有自己的地址。通俗的理解,地址是給變量/常量來存放值的地點
  3. 路牌:注意注意注意!這裏就指出了變量/常量小S的地址:神仙小區403
    事實上,我們等會會講,輸出一個變量的地址其實是個16進制的数字。

搞懂了上面,我們再來聊聊&
&這個符號我們一個不陌生,你最初用到應該是在:scanf("%d",&a)裡邊。
&叫做取址符,用來獲取一個變量/常量的地址。
那麼我們為什麼要在scanf裡邊用&,不在printf裡邊用呢?
一開始我也很疑惑,後來我看到了這個例子:
你是一個新生,你要進教室。
但是你並不知道教室在哪裡,這個時候你需要教室的地址。
下課了,你要出教室。
由於你已經在教室里了,你就不需要獲取教室的地址就可以出去了。

Part 2:一定要記住的東西

一定要記住:指針就是個變量!
重要的事情說三次:
指針就是個變量!他儲存的是地址!他自己也有地址!
指針就是個變量!他儲存的是地址!他自己也有地址!
指針就是個變量!他儲存的是地址!他自己也有地址!

為什麼這麼說?我們從指針的定義開始:

指針的定義方法:<類型名+*> [名稱]
也就是說,指針的定義大概是這樣的:

int* ip;            //類型是int*,名稱是ip
float* fp;          //類型是float*,名稱是fp
double* dp;         //類型是double*,名稱是dp

有的書上會這麼寫:

int *ip;
float *fp;
double *dp;

這麼寫當然沒問題,但是對於初學者來說,有兩個問題:

  1. 有的初學者會把*p當做是指針名
  2. 有的初學者會把定義時出現的*p取值時出現的*p弄混

指針他有沒有值?有!我們會在下一節給他賦值。
既然他的定義方式和變量一樣,他也有值,他為什麼不是變量呢?

Part 3:與指針相關的幾個符號

與指針相關的符號有兩個,一個是&,一個是*
先來聊聊&
&我們上面講過,他是來取地址的。舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    float b = 10.3;
    printf("%p,%p",&a,&b);
}

%p用來輸出地址,當然,你也可以寫成%d或者%x。先不管這個,我們來看看他會輸出什麼:

那麼也就是說,變量ab的地址是000000000062FE1C000000000062FE18
那麼我們怎麼把這個地址給指針呢?很簡單:p = &a;,舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("a的地址:%p\n",&a);
    printf("指針p自身的地址:%p\n",&p);
    printf("指針p指向的地址:%p",p);
}

得到輸出:

a的地址:000000000062FE1C
指針p自身的地址:000000000062FE10
指針p指向的地址:000000000062FE1C

你發現了嗎?如果我們有p = &a;,我們發現:直接輸出p會輸出a的地址,輸出&p會輸出p的地址(這就是為什麼我一再強調p是個變量,他有自己的地址,正如路牌上有地址,路牌自身也有個地址一樣)。

請注意!如果你的指針為int*,那麼你只能指向int類型;如果是double*類型,只能指向double類型,以此類推

當然,void*類型的指針可以轉化為任何一種不同的指針類型(如int*,double*等等)

那麼,我們來聊聊第二個符號*
*有兩個用法。第一個在定義指針時用到,第二個則是取值,什麼意思?看下面這個例子:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("a的地址:%p\n",&a);
    printf("指針p自身的地址:%p\n",&p);
    printf("指針p指向的地址:%p\n",p);
    printf("指針p指向的地址的值:%d",*p);
}

得到輸出:

a的地址:000000000062FE1C
指針p自身的地址:000000000062FE10
指針p指向的地址:000000000062FE1C
指針p指向的地址的值:10

哈,我們得到了a的值!
也就是說,當我們有p = &a,我們可以用*p得到a的值。
那能不能操作呢?當然可以。
我們可以把*p當做a的值,那麼,我們嘗試如下代碼:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("指針p指向的地址的值:%d\n",*p);
    *p = 13;
    printf("指針p指向的地址的值:%d\n",*p);
    *p += 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p -= 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p *= 9;
    printf("指針p指向的地址的值:%d\n",*p);
    *p /= 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p %= 3;
    printf("指針p指向的地址的值:%d\n",*p);
}

得到輸出:

指針p指向的地址的值:10
指針p指向的地址的值:13
指針p指向的地址的值:16
指針p指向的地址的值:13
指針p指向的地址的值:117
指針p指向的地址的值:39
指針p指向的地址的值:0

棒極了!我們可以用指針來操作變量了。
那麼,我們要這個干什麼用呢?請看下一節:實現交換函數

Part 4:交換函數

交換函數是指針必學的一個東西。一般的交換我們會這麼寫:

t = a;
a = b;
b = t;

那麼我們把它塞到函數裡邊:

void swap(int a,int b){
      int t;
      t = a;
      a = b;
      b = t;
}

好,我們滿懷信心的調用他:

#include <stdio.h>
void swap(int a,int b){
      int t;
      t = a;
      a = b;
      b = t;
}
int main(){
      int x = 5,y = 10;
      printf("x=%d,y=%d\n",x,y);
      swap(x,y);
      printf("x=%d,y=%d",x,y);
}

於是乎,你得到了這個輸出:

x=5,y=10
x=5,y=10

啊啊啊啊啊啊啊啊,為什麼不行!!!
問題就在你的swap函數,我們來看看他們做了些啥:

swap(x,y);             --->把x賦值給a,把y賦值給b
///進入函數體
int t;                 --->定義t
t = a;                 --->t賦值為a
a = b;                 --->a賦值為b
b = t;                 --->b賦值為t

各位同學,函數體內有任何一點談到了x和y嗎?
所謂的交換,交換的到底是a和b,還是x和y?
我相信你這時候你恍然大悟了,我們一直在交換a和b,並沒有操作x和y

那麼我們怎麼操作?指針!
因為x和y在整個程序中的地址一定是不變的,那麼我們通過上一節的指針運算可以得到,我們能夠經過指針操作變量的值。
那麼,我們改進一下這個函數

void swap(int* a,int* b){
      int t;
      t = *a;
      *a = *b;
      *b = t;
}

我們再來試試,然後你就會得到報錯信息。

我想,你是這麼用的:swap(x,y)
問題就在這裏,我們看看swap需要怎樣的兩個變量?int*int*類型。
怎麼辦?我告訴你一個小秘密:
任何一個變量加上&,此時就相當於在原本的類型加上了*
什麼意思?也就是說:

int a;
&a ---> int*;
double d;
&d ---> double*;
int* p;
&p ---> int**;//這是個二級指針,也就是說指向指針的指針

那麼,我們要這麼做:swap(&a,&b),把傳入的參數int換為int*

再次嘗試,得到輸出:

x=5,y=10
x=10,y=5

累死了,總算是搞好了

Part 5:char*表示字符串

char*這個神奇的類型可以表示個字符串,舉個例子:


#include <stdio.h>

int main()
{
    char* str;
    str = "YOU AK IOI!";
    printf("%s",str);
}

請注意:輸入和輸出字符串的時候,都不能帶上*&

你可以用string.h中的函數來進行操作

Part 6:野指針

有些同學他會這麼寫:

int* p;
printf("%p",p);

哦千萬不要這麼做!
當你沒有讓p指向某個地方的時候,你還把他用了!這個時候就會產生野指針。
野指針的危害是什麼?
第一種是指向不可訪問(操作系統不允許訪問的敏感地址,譬如內核空間)的地址,結果是觸發段錯誤,這種算是最好的情況了;

第二種是指向一個可用的、而且沒什麼特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程序運行不會出錯,也不會對當前程序造成損害,這種情況下會掩蓋你的程序錯誤,讓你以為程序沒問題,其實是有問題的;

第三種情況就是指向了一個可用的空間,而且這個空間其實在程序中正在被使用(譬如說是程序的一個變量x),那麼野指針的解引用就會剛好修改這個變量x的值,導致這個變量莫名其妙的被改變,程序出現離奇的錯誤。一般最終都會導致程序崩潰,或者數據被損害。這種危害是最大的。

不論如何,我們都不希望看到這些發生。
於是,養成好習慣:變量先賦值。

指針你可以這麼做:int *p =NULL;讓指針指向空

不論如何,他總算有個值了。

Part 7:總結

本文乾貨全部在這裏了:

  1. 指針是個變量,他的類型是數據類型+*,他的值是一個地址,他自身也有地址
  2. 指針有兩個專屬運算符:&*
  3. 指針可以操作變量,不能操作常量
  4. 指針可以表示字符串
  5. 請注意野指針的問題

本文沒有講到的:

  1. char[],char,const char的區別與聯繫
  2. const修飾指針會怎麼樣?
  3. void*指針的運用
  4. 多級指針的運用
  5. NULL到底是什麼
  6. malloc函數的運用

感謝觀看!

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

※回頭車貨運收費標準

軟件設計模式學習(二十四)狀態模式

狀態模式用於解決系統中複雜對象的狀態轉換以及不同狀態下行為的封裝問題

模式動機

很多情況下,一個對象的行為取決於一個或多個動態變化的屬性,這樣的屬性叫做狀態。一個對象可以擁有多個狀態,這些狀態可以相互轉換,當對象狀態不同時,其行為也有所差異。

假設一個人就是對象,人根據心情不同會有很多狀態,比如開心和傷心,這兩種狀態可以相互轉換。開心的人可能會突然接到女朋友的分手電話,然後哭得稀里嘩啦(醒醒!你哪來的女朋友?),過了一段時間后,又可能因為中了一百萬彩票而歡呼雀躍。而且不同狀態下人的行為也不同,有些人傷心時會通過運動、旅行、聽音樂來緩解心情,而開心時則可能會唱歌、跳舞、請客吃飯等等。

再來考慮軟件系統中的情況,如某酒店訂房系統,可以將房間設計為一個類,房間對象有已預訂、空閑、已入住等情況,這些狀態之間可以相互轉換,並且不同狀態的對象可能具有不同的行為,如已預訂或已入住的房間不能再接收其他顧客的預訂,而空閑的房間可以接受預訂。

在過去我們遇到這種情況,可以使用複雜的條件判斷來進行狀態判斷和轉換操作,這會導致代碼的可維護性和靈活性下降,當出現新的狀態時必須修改源代碼,違反了開閉原則。在狀態模式中,可以將對象狀態從包含該狀態的類中分離出來,做成一個個單獨的狀態類,如人的兩種情緒可以設計成兩個狀態類:

將開心與傷心兩種情緒從“人”中分離出來,從而避免在“人”中進行狀態轉換和判斷,將擁有狀態的對象和狀態對應的行為分離,這就是狀態模式的動機。

模式定義

允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。其別名為狀態對象(Objects for States),狀態模式是一種對象行為型模式。

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

模式結構與分析

我們把擁有狀態的對象稱為環境類,也叫上下文類。再引入一個抽象狀態類來專門表示對象的狀態,對象的每一種具體狀態類都繼承該抽象類,不同具體狀態類實現不同狀態的行為,包括各種狀態之間的轉換。在環境類中維護一個抽象狀態類 State 的實例,用來定義當前狀態。

得到狀態模式結構類圖如下:

環境類中的 request() 方法處理業務邏輯,根據狀態去調用對應的 handle() 方法,如果需要切換狀態,還提供了 setState() 用於設置當前房間狀態。如果我們希望執行操作后狀態自動發生改變,那麼我們還需要在 State 中定義一個 Context 對象,實現一個雙向依賴關係。

考慮前面提到的訂房系統,如果不使用狀態模式,可能就會存在如下代碼:

if (state == "空閑") {
	if (預訂房間) {
        預訂操作;
        state = "已預訂";
	} else if (住進房間) {
    	入住操作;
        state = "已入住";
    }
} else if(state == "已預訂") {
	if (住進房間) {
        入住操作;
        state = "已入住";
	} else if (取消預訂) {
    	取消操作;
        state = "空閑";
    }
}

上述代碼需要做頻繁且複雜的判斷操作,可維護性很差。因此考慮使用狀態模式將房間類的狀態分離出來,將與每種狀態有關的操作封裝在獨立的狀態類中。

我們來寫一個完整的示例

環境類(Room)

public class Room {
	
    // 維護一個狀態對象
    private State state;

    public Room() {
        // 默認為空閑狀態
        this.state = new IdleState(this);
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public void reserve() {
        state.reserve();
    }

    public void checkIn() {
        state.checkIn();
    }

    public void cancelReserve() {
        state.cancelReserve();
    }

    public void checkOut() {
        state.checkOut();
    }
}

抽象狀態類(State)

public abstract class State {
	
    // 用於狀態轉換
    protected Room room;

    public State(Room room) {
        this.room = room;
    }

    public abstract void reserve();

    public abstract void checkIn();

    public abstract void cancelReserve();

    public abstract void checkOut();
}

具體狀態類(IdleState)

public class IdleState extends State {

    public IdleState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("房間預訂成功");
        // 	切換狀態
        room.setState(new ReservedState(room));
    }

    @Override
    public void checkIn() {
        System.out.println("房間入住成功");
        room.setState(new InhabitedState(room));
    }

    @Override
    public void cancelReserve() {
        System.out.println("無法取消預訂,房間處於空閑狀態");
    }

    @Override
    public void checkOut() {
        System.out.println("無法退房,房間處於空閑狀態");
    }

}

具體狀態類(ReservedState)

public class ReservedState extends State {

    public ReservedState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("無法預訂,房間處於已預訂狀態");
    }

    @Override
    public void checkIn() {
        System.out.println("房間入住成功");
        room.setState(new InhabitedState(room));
    }

    @Override
    public void cancelReserve() {
        System.out.println("取消預訂成功");
        room.setState(new IdleState(room));
    }

    @Override
    public void checkOut() {
        System.out.println("無法退房,房間處於已預訂狀態");
    }
}

具體狀態類(InhabitedState)

public class InhabitedState extends State {

    public InhabitedState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("無法預訂,房間處於入住狀態");
    }

    @Override
    public void checkIn() {
        System.out.println("無法入住,房間處於入住狀態");
    }

    @Override
    public void cancelReserve() {
        System.out.println("無法取消預訂,房間處於入住狀態");
    }

    @Override
    public void checkOut() {
        System.out.println("退房成功");
        room.setState(new IdleState(room));
    }
}

客戶端測試類(Client)

public class Client {

    public static void main(String[] args) {

        Room room = new Room();
        room.cancelReserve();
        room.checkOut();
        room.reserve();
        System.out.println("--------------------------");
        room.reserve();
        room.checkOut();
        room.checkIn();
        System.out.println("--------------------------");
        room.reserve();
        room.checkIn();
        room.cancelReserve();
        room.checkOut();
    }
}

運行結果

模式優缺點

狀態模式的優點:

  • 封裝了轉換規則,將不同狀態之間的轉換狀態封裝在狀態類中,避免了冗長的條件判斷,提高了代碼的可維護性
  • 將所有與某個規則有關的行為放到一個類,可以很方便地增加新的狀態
  • 可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數

狀態模式的缺點:

  • 增加了系統類和對象的個數
  • 結構較為複雜,使用不當將導致代碼混亂
  • 對於可以切換狀態的狀態模式,增加新的狀態類需要修改負責狀態轉換的代碼

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

【其他文章推薦】

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

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

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

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

※回頭車貨運收費標準

對於單例模式面試官會怎樣提問呢?你又該如何回答呢?

前言

在面試的時候面試官會怎麼在單例模式中提問呢?你又該如何回答呢?可能你在面試的時候你會碰到這些問題:

  • 為什麼說餓漢式單例天生就是線程安全的?

  • 傳統的懶漢式單例為什麼是非線程安全的?

  • 怎麼修改傳統的懶漢式單例,使其線程變得安全?

  • 線程安全的單例的實現還有哪些,怎麼實現?

  • 雙重檢查模式、Volatile關鍵字 在單例模式中的應用

  • ThreadLocal 在單例模式中的應用

  • 枚舉式單例

那我們該怎麼回答呢?那答案來了,看完接下來的內容就可以跟面試官嘮嘮單例模式了

 

單例模式簡介

單例模式是一種常用的軟件設計模式,其屬於創建型模式,其含義即是一個類只有一個實例,併為整個系統提供一個全局訪問點 (向整個系統提供這個實)。

結構:

                      

單例模式三要素:

  • 私有的構造方法;

  • 私有靜態實例引用;

  • 返回靜態實例的靜態公有方法。

單例模式的優點

  • 在內存中只有一個對象,節省內存空間;

  • 避免頻繁的創建銷毀對象,可以提高性能;

  • 避免對共享資源的多重佔用,簡化訪問;

  • 為整個系統提供一個全局訪問點。

單例模式的注意事項

  在使用單例模式時,我們必須使用單例類提供的公有工廠方法得到單例對象,而不應該使用反射來創建,使用反射將會破壞單例模式 ,將會實例化一個新對象。

 

單線程實現方式

在單線程環境下,單例模式根據實例化對象時機的不同分為,

  • 餓漢式單例(立即加載)餓漢式單例在單例類被加載時候,就實例化一個對象並將引用所指向的這個實例;

  • 懶漢式單例(延遲加載),只有在需要使用的時候才會實例化一個對象將引用所指向的這個實例。

 

從速度和反應時間角度來講,餓漢式(又稱立即加載)要好一些;從資源利用效率上說,懶漢式(又稱延遲加載)要好一些。

餓漢式單例

// 餓漢式單例
public class HungrySingleton{
​
    // 私有靜態實例引用,創建私有靜態實例,並將引用所指向的實例
    private static HungrySingleton singleton = new HungrySingleton();
    // 私有的構造方法
    private HungrySingleton(){}
    //返回靜態實例的靜態公有方法,靜態工廠方法
    public static HungrySingleton getSingleton(){
        return singleton;
    }
}

餓漢式單例,在類被加載時,就會實例化一個對象並將引用所指向的這個實例;更重要的是,由於這個類在整個生命周期中只會被加載一次,只會被創建一次,因此惡漢式單例線程安全的。

那餓漢式單例為什麼是天生就線程安全呢?

因為類加載的方式是按需加載,且只加載一次。由於一個類在整個生命周期中只會被加載一次,在線程訪問單例對象之前就已經創建好了,且僅此一個實例。即線程每次都只能也必定只可以拿到這個唯一的對象。

懶漢式單例

// 懶漢式單例
public class LazySingleton {
    // 私有靜態實例引用
    private static LazySingleton singleton;
    // 私有的構造方法
    private LazySingleton(){}
    // 返回靜態實例的靜態公有方法,靜態工廠方法
    public static LazySingleton getSingleton(){
        //當需要創建類的時候創建單例類,並將引用所指向的實例
        if (singleton == null) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

懶漢式單例是延遲加載,只有在需要使用的時候才會實例化一個對象,並將引用所指向的這個對象。

由於是需要時創建,在多線程環境是不安全的,可能會併發創建實例,出現多實例的情況,單例模式的初衷是相背離的。那我們需要怎麼避免呢?可以看接下來的多線程中單例模式的實現形式。

那為什麼傳統的懶漢式單例為什麼是非線程安全的?

非線程安全主要原因是,會有多個線程同時進入創建實例(if (singleton == null) {}代碼塊)的情況發生。當這種這種情形發生后,該單例類就會創建出多個實例,違背單例模式的初衷。因此,傳統的懶漢式單例是非線程安全的。

 

多線程實現方式

  在單線程環境下,無論是餓漢式單例還是懶漢式單例,它們都能夠正常工作。但是,在多線程環境下就有可能發生變異:

  • 餓漢式單例天生就是線程安全的,可以直接用於多線程而不會出現問題

  • 懶漢式單例本身是非線程安全的,因此就會出現多個實例的情況,與單例模式的初衷是相背離的。

 

那我們應該怎麼在懶漢的基礎上改造呢?

  • synchronized方法

  • synchronized塊

  • 使用內部類實現延遲加載

synchronized方法

// 線程安全的懶漢式單例
public class SynchronizedSingleton {
    private static SynchronizedSingleton synchronizedSingleton;
    private SynchronizedSingleton(){}
    // 使用 synchronized 修飾,臨界資源的同步互斥訪問
    public static synchronized SynchronizedSingleton getSingleton(){
        if (synchronizedSingleton == null) {
            synchronizedSingleton = new SynchronizedSingleton();
        }
        return synchronizedSingleton;
    }
}

  使用 synchronized 修飾 getSingleton()方法,將getSingleton()方法進行加鎖,實現對臨界資源的同步互斥訪問,以此來保證單例。

雖然可現實線程安全,但由於同步的作用域偏大、鎖的粒度有點粗,會導致運行效率會很低。

synchronized塊

// 線程安全的懶漢式單例
public class BlockSingleton {
    private static BlockSingleton singleton;
    private BlockSingleton(){}
    public static BlockSingleton getSingleton2(){
        synchronized(BlockSingleton.class){  // 使用 synchronized 塊,臨界資源的同步互斥訪問
            if (singleton == null) { 
                singleton = new BlockSingleton();
            }
        }
        return singleton;
    }
}

 其實synchronized塊跟synchronized方法類似,效率都偏低。

使用內部類實現延遲加載

// 線程安全的懶漢式單例
public class InsideSingleton {
    // 私有內部類,按需加載,用時加載,也就是延遲加載
    private static class Holder {
        private static InsideSingleton insideSingleton = new InsideSingleton();
    }
    private InsideSingleton() {
    }
    public static InsideSingleton getSingleton() {
        return Holder.insideSingleton;
    }
}
  • 如上述代碼所示,我們可以使用內部類實現線程安全的懶漢式單例,這種方式也是一種效率比較高的做法。其跟餓漢式單例原理是相同的, 但可能還存在反射攻擊或者反序列化攻擊 。

 

雙重檢查(Double-Check idiom)現實

雙重檢查(Double-Check idiom)-volatile

使用雙重檢測同步延遲加載去創建單例,不但保證了單例,而且提高了程序運行效率。

// 線程安全的懶漢式單例
public class DoubleCheckSingleton {
    //使用volatile關鍵字防止重排序,因為 new Instance()是一個非原子操作,可能創建一個不完整的實例
    private static volatile DoubleCheckSingleton singleton;
    private DoubleCheckSingleton() {
    }
​
    public static DoubleCheckSingleton getSingleton() {
        // Double-Check idiom
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {       
                // 只需在第一次創建實例時才同步
                if (singleton == null) {      
                    singleton = new DoubleCheckSingleton();      
                }
            }
        }
        return singleton;
    }
​
}

為了在保證單例的前提下提高運行效率,我們需要對singleton實例進行第二次檢查,為的式避開過多的同步(因為同步只需在第一次創建實例時才同步,一旦創建成功,以後獲取實例時就不需要同步獲取鎖了)。

但需要注意的必須使用volatile關鍵字修飾單例引用,為什麼呢?

 如果沒有使用volatile關鍵字是可能會導致指令重排序情況出現,在Singleton 構造函數體執行之前,變量 singleton可能提前成為非 null 的,即賦值語句在對象實例化之前調用,此時別的線程將得到的是一個不完整(未初始化)的對象,會導致系統崩潰。

此可能為程序執行步驟:

  1. 線程 1 進入 getSingleton() 方法,由於 singleton 為 null,線程 1 進入 synchronized 塊 ;

  2. 同樣由於 singleton為 null,線程 1 直接前進到 singleton = new DoubleCheckSingleton()處,在new對象的時候出現重排序,導致在構造函數執行之前,使實例成為非 null,並且該實例並未初始化的(原因在NOTE);

  3. 此時,線程 2 檢查實例是否為 null。由於實例不為 null,線程 2 得到一個不完整(未初始化)的 Singleton 對象

  4. 線程 1 通過運行 Singleton對象的構造函數來完成對該對象的初始化。

  這種安全隱患正是由於指令重排序的問題所導致的。而volatile 關鍵字正好可以完美解決了這個問題。使用volatile關鍵字修飾單例引用就可以避免上述災難。

NOTE

new 操作會進行三步走,預想中的執行步驟:

memory = allocate();        //1:分配對象的內存空間
ctorInstance(memory);       //2:初始化對象
singleton = memory;        //3:使singleton3指向剛分配的內存地址

**但實際上,這個過程可能發生無序寫入(指令重排序),可能會導致所下執行步驟:

memory = allocate();        //1:分配對象的內存空間
singleton3 = memory;        //3:使singleton3指向剛分配的內存地址
ctorInstance(memory);       //2:初始化對象

雙重檢查(Double-Check idiom)-ThreadLocal

  藉助於 ThreadLocal,我們可以實現雙重檢查模式的變體。我們將臨界資源線程局部化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換為 線程局部範圍內的操作 。

// 線程安全的懶漢式單例
public class ThreadLocalSingleton 
    // ThreadLocal 線程局部變量
    private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>();
    private static ThreadLocalSingleton singleton = null;
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getSingleton(){
        if (threadLocal.get() == null) {        // 第一次檢查:該線程是否第一次訪問
            createSingleton();
        }
        return singleton;
    }
​
    public static void createSingleton(){
        synchronized (ThreadLocalSingleton.class) {
            if (singleton == null) {          // 第二次檢查:該單例是否被創建
                singleton = new ThreadLocalSingleton();   // 只執行一次
            }
        }
        threadLocal.set(singleton);      // 將單例放入當前線程的局部變量中 
    }
}

藉助於 ThreadLocal,我們也可以實現線程安全的懶漢式單例。但與直接雙重檢查模式使用,使用ThreadLocal的實現在效率上還不如雙重檢查鎖定。

 

枚舉實現方式

它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,

直接通過Singleton.INSTANCE.whateverMethod()的方式調用即可。方便、簡潔又安全。

public enum EnumSingleton {
    instance;
    public void whateverMethod(){
        //dosomething
    }
}

 

 

測試單例線程安全性

 使用多個線程,並使用hashCode值計算每個實例的值,值相同為同一實例,否則為不同實例。

public class Test {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();
​
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
​
        }
    }
}
class TestThread extends Thread {
    @Override
    public void run() {
        // 對於不同單例模式的實現,只需更改相應的單例類名及其公有靜態工廠方法名即可
        int hash = Singleton5.getSingleton5().hashCode();  
        System.out.println(hash);
    }
}

 

 

小結

單例模式是 Java 中最簡單,也是最基礎,最常用的設計模式之一。在運行期間,保證某個類只創建一個實例,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點 ,介紹單例模式的各種寫法:

  • 餓漢式單例(線程安全)

  • 懶漢式單例

    • 傳統懶漢式單例(線程安全);

    • 使用synchronized方法實(線程安全);

    • 使用synchronized塊實現懶漢式單例(線程安全);

    • 使用靜態內部類實現懶漢式單例(線程安全)。

  • 使用雙重檢查模式

    • 使用volatile關鍵字(線程安全);

    • 使用ThreadLocal實現懶漢式單例(線程安全)。

  • 枚舉式單例

 

各位看官還可以嗎?喜歡的話,動動手指點個,點個關注唄!!謝謝支持! 歡迎關注公眾號【Ccww技術博客】,原創技術文章第一時間推出

 

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

全球最大豬肉加工商因染疫關廠 警告肉類恐短缺

摘錄自2020年4月13日中央社報導

全球最大豬肉加工商史密斯菲爾德食品公司(Smithfield Foods)今(12日)指出,因為有數百員工感染武漢肺炎,將無限期關閉美國的一家豬肉工廠。這家公司也警告,疫情大流行期間可能出現肉類供應短缺。

美國南達科他州州長諾埃姆(Kristi Noem)昨天表示,設在南達科他州蘇瀑市(Sioux Falls)的史密斯菲爾德食品公司豬肉工廠,有238名員工感染2019冠狀病毒疾病(COVID-19,武漢肺炎),占全州確診總數的55%。

諾埃姆與蘇瀑市(Sioux Falls)市長譚哈肯(Paul TenHaken)建議,史密斯菲爾德食品公司應關閉發生疫情的工廠至少兩週。這座工廠約有3700員工,是全美最大豬肉加工廠之一,占美國豬肉產量的4%至5%。

生活環境
國際新聞
豬肉
加工
疫情下的食衣住行
武漢肺炎
食品安全

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準